Compare commits

..

3 Commits

Author SHA1 Message Date
6be2c02506 fix: socket connection 2024-09-01 22:04:56 -04:00
1d92b6ea6f chore: removed logs, added comments 2024-09-01 20:06:11 -04:00
f683ff6480 fix: files not loading when creating a new project
This push contains console logs at various places where the server is emitting the event and the client is receiving the event. Please remove those before merging with production.
2024-08-31 20:31:20 -04:00
39 changed files with 666 additions and 4623 deletions

View File

@ -7,9 +7,6 @@
"": { "": {
"name": "ai", "name": "ai",
"version": "0.0.0", "version": "0.0.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.27.2"
},
"devDependencies": { "devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.1.0", "@cloudflare/vitest-pool-workers": "^0.1.0",
"@cloudflare/workers-types": "^4.20240512.0", "@cloudflare/workers-types": "^4.20240512.0",
@ -18,28 +15,6 @@
"wrangler": "^3.0.0" "wrangler": "^3.0.0"
} }
}, },
"node_modules/@anthropic-ai/sdk": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.27.2.tgz",
"integrity": "sha512-Q6gOx4fyHQ+NCSaVeXEKFZfoFWCR3ctUA+sK5oGB7RKUkzUvK64aYM7v1T9ekJKwn8TwRq6IGjqS31n9PbjCIA==",
"dependencies": {
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
"abort-controller": "^3.0.0",
"agentkeepalive": "^4.2.1",
"form-data-encoder": "1.7.2",
"formdata-node": "^4.3.2",
"node-fetch": "^2.6.7"
}
},
"node_modules/@anthropic-ai/sdk/node_modules/@types/node": {
"version": "18.19.50",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.50.tgz",
"integrity": "sha512-xonK+NRrMBRtkL1hVCc3G+uXtjh1Al4opBLjqVmipe5ZAaBYWW6cNAiBVZ1BvmkBhep698rP3UM3aRAdSALuhg==",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@cloudflare/kv-asset-handler": { "node_modules/@cloudflare/kv-asset-handler": {
"version": "0.3.2", "version": "0.3.2",
"resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.2.tgz", "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.2.tgz",
@ -886,19 +861,11 @@
"version": "20.12.11", "version": "20.12.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz",
"integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==", "integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==",
"dev": true,
"dependencies": { "dependencies": {
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
}, },
"node_modules/@types/node-fetch": {
"version": "2.6.11",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz",
"integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==",
"dependencies": {
"@types/node": "*",
"form-data": "^4.0.0"
}
},
"node_modules/@types/node-forge": { "node_modules/@types/node-forge": {
"version": "1.3.11", "version": "1.3.11",
"resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz",
@ -977,17 +944,6 @@
"url": "https://opencollective.com/vitest" "url": "https://opencollective.com/vitest"
} }
}, },
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.11.3", "version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
@ -1009,17 +965,6 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/agentkeepalive": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz",
"integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==",
"dependencies": {
"humanize-ms": "^1.2.1"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/ansi-styles": { "node_modules/ansi-styles": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
@ -1063,11 +1008,6 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -1201,17 +1141,6 @@
"integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==",
"dev": true "dev": true
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": { "node_modules/commander": {
"version": "12.0.0", "version": "12.0.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz",
@ -1285,14 +1214,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/devalue": { "node_modules/devalue": {
"version": "4.3.3", "version": "4.3.3",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.3.tgz", "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.3.tgz",
@ -1366,14 +1287,6 @@
"@types/estree": "^1.0.0" "@types/estree": "^1.0.0"
} }
}, },
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/execa": { "node_modules/execa": {
"version": "8.0.1", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
@ -1421,36 +1334,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/form-data-encoder": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="
},
"node_modules/formdata-node": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
"dependencies": {
"node-domexception": "1.0.0",
"web-streams-polyfill": "4.0.0-beta.3"
},
"engines": {
"node": ">= 12.20"
}
},
"node_modules/fs.realpath": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -1569,14 +1452,6 @@
"node": ">=16.17.0" "node": ">=16.17.0"
} }
}, },
"node_modules/humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
"dependencies": {
"ms": "^2.0.0"
}
},
"node_modules/inflight": { "node_modules/inflight": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@ -1735,25 +1610,6 @@
"node": ">=10.0.0" "node": ">=10.0.0"
} }
}, },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mimic-fn": { "node_modules/mimic-fn": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
@ -1819,7 +1675,8 @@
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
}, },
"node_modules/mustache": { "node_modules/mustache": {
"version": "4.2.0", "version": "4.2.0",
@ -1848,43 +1705,6 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
} }
}, },
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-forge": { "node_modules/node-forge": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
@ -2402,11 +2222,6 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/ts-json-schema-generator": { "node_modules/ts-json-schema-generator": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/ts-json-schema-generator/-/ts-json-schema-generator-1.5.1.tgz", "resolved": "https://registry.npmjs.org/ts-json-schema-generator/-/ts-json-schema-generator-1.5.1.tgz",
@ -2477,7 +2292,8 @@
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "5.26.5", "version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.2.11", "version": "5.2.11",
@ -3011,28 +2827,6 @@
} }
} }
}, },
"node_modules/web-streams-polyfill": {
"version": "4.0.0-beta.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
"engines": {
"node": ">= 14"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -15,8 +15,5 @@
"typescript": "^5.0.4", "typescript": "^5.0.4",
"vitest": "1.3.0", "vitest": "1.3.0",
"wrangler": "^3.0.0" "wrangler": "^3.0.0"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.27.2"
} }
} }

View File

@ -1,121 +1,43 @@
import { Anthropic } from "@anthropic-ai/sdk";
import { MessageParam } from "@anthropic-ai/sdk/src/resources/messages.js";
export interface Env { export interface Env {
ANTHROPIC_API_KEY: string; AI: any
} }
export default { export default {
async fetch(request: Request, env: Env): Promise<Response> { async fetch(request, env): Promise<Response> {
// Handle CORS preflight requests if (request.method !== "GET") {
if (request.method === "OPTIONS") { return new Response("Method Not Allowed", { status: 405 })
return new Response(null, { }
headers: {
"Access-Control-Allow-Origin": "*", const url = new URL(request.url)
"Access-Control-Allow-Methods": "GET, POST, OPTIONS", const fileName = url.searchParams.get("fileName")
"Access-Control-Allow-Headers": "Content-Type", const instructions = url.searchParams.get("instructions")
const line = url.searchParams.get("line")
const code = url.searchParams.get("code")
const response = await env.AI.run("@cf/meta/llama-3-8b-instruct", {
messages: [
{
role: "system",
content:
"You are an expert coding assistant. You read code from a file, and you suggest new code to add to the file. You may be given instructions on what to generate, which you should follow. You should generate code that is CORRECT, efficient, and follows best practices. You may generate multiple lines of code if necessary. When you generate code, you should ONLY return the code, and nothing else. You MUST NOT include backticks in the code you generate.",
}, },
}); {
} role: "user",
content: `The file is called ${fileName}.`,
if (request.method !== "GET" && request.method !== "POST") {
return new Response("Method Not Allowed", { status: 405 });
}
let body;
let isEditCodeWidget = false;
if (request.method === "POST") {
body = await request.json() as { messages: unknown; context: unknown; activeFileContent: string };
} else {
const url = new URL(request.url);
const fileName = url.searchParams.get("fileName") || "";
const code = url.searchParams.get("code") || "";
const line = url.searchParams.get("line") || "";
const instructions = url.searchParams.get("instructions") || "";
body = {
messages: [{ role: "human", content: instructions }],
context: `File: ${fileName}\nLine: ${line}\nCode:\n${code}`,
activeFileContent: code,
};
isEditCodeWidget = true;
}
const messages = body.messages;
const context = body.context;
const activeFileContent = body.activeFileContent;
if (!Array.isArray(messages) || messages.length === 0) {
return new Response("Invalid or empty messages", { status: 400 });
}
let systemMessage;
if (isEditCodeWidget) {
systemMessage = `You are an AI code editor. Your task is to modify the given code based on the user's instructions. Only output the modified code, without any explanations or markdown formatting. The code should be a direct replacement for the existing code.
Context:
${context}
Active File Content:
${activeFileContent}
Instructions: ${messages[0].content}
Respond only with the modified code that can directly replace the existing code.`;
} else {
systemMessage = `You are an intelligent programming assistant. Please respond to the following request concisely. If your response includes code, please format it using triple backticks (\`\`\`) with the appropriate language identifier. For example:
\`\`\`python
print("Hello, World!")
\`\`\`
Provide a clear and concise explanation along with any code snippets. Keep your response brief and to the point.
${context ? `Context:\n${context}\n` : ''}
${activeFileContent ? `Active File Content:\n${activeFileContent}\n` : ''}`;
}
const anthropicMessages = messages.map(msg => ({
role: msg.role === 'human' ? 'user' : 'assistant',
content: msg.content
})) as MessageParam[];
try {
const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY });
const stream = await anthropic.messages.create({
model: "claude-3-5-sonnet-20240620",
max_tokens: 1024,
system: systemMessage,
messages: anthropicMessages,
stream: true,
});
const encoder = new TextEncoder();
const streamResponse = new ReadableStream({
async start(controller) {
for await (const chunk of stream) {
if (chunk.type === 'content_block_delta' && chunk.delta.type === 'text_delta') {
const bytes = encoder.encode(chunk.delta.text);
controller.enqueue(bytes);
}
}
controller.close();
}, },
}); {
role: "user",
content: `Here are my instructions on what to generate: ${instructions}.`,
},
{
role: "user",
content: `Suggest me code to insert at line ${line} in my file. Give only the code, and NOTHING else. DO NOT include backticks in your response. My code file content is as follows
return new Response(streamResponse, { ${code}`,
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Access-Control-Allow-Origin": "*",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
}, },
}); ],
} catch (error) { })
console.error("Error:", error);
return new Response("Internal Server Error", { status: 500 }); return new Response(JSON.stringify(response))
}
}, },
}; } satisfies ExportedHandler<Env>

View File

@ -12,7 +12,7 @@
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"e2b": "^0.16.2-beta.47", "e2b": "^0.16.1",
"express": "^4.19.2", "express": "^4.19.2",
"rate-limiter-flexible": "^5.0.3", "rate-limiter-flexible": "^5.0.3",
"simple-git": "^3.25.0", "simple-git": "^3.25.0",
@ -41,28 +41,6 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@bufbuild/protobuf": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz",
"integrity": "sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag=="
},
"node_modules/@connectrpc/connect": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-1.4.0.tgz",
"integrity": "sha512-vZeOkKaAjyV4+RH3+rJZIfDFJAfr+7fyYr6sLDKbYX3uuTVszhFe9/YKf5DNqrDb5cKdKVlYkGn6DTDqMitAnA==",
"peerDependencies": {
"@bufbuild/protobuf": "^1.4.2"
}
},
"node_modules/@connectrpc/connect-web": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@connectrpc/connect-web/-/connect-web-1.4.0.tgz",
"integrity": "sha512-13aO4psFbbm7rdOFGV0De2Za64DY/acMspgloDlcOKzLPPs0yZkhp1OOzAQeiAIr7BM/VOHIA3p8mF0inxCYTA==",
"peerDependencies": {
"@bufbuild/protobuf": "^1.4.2",
"@connectrpc/connect": "1.4.0"
}
},
"node_modules/@cspotcode/source-map-support": { "node_modules/@cspotcode/source-map-support": {
"version": "0.8.1", "version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@ -465,7 +443,6 @@
"integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"node-gyp-build": "^4.3.0" "node-gyp-build": "^4.3.0"
}, },
@ -587,11 +564,6 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
}, },
"node_modules/compare-versions": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz",
"integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg=="
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -765,19 +737,23 @@
} }
}, },
"node_modules/e2b": { "node_modules/e2b": {
"version": "0.16.2-beta.47", "version": "0.16.2",
"resolved": "https://registry.npmjs.org/e2b/-/e2b-0.16.2-beta.47.tgz", "resolved": "https://registry.npmjs.org/e2b/-/e2b-0.16.2.tgz",
"integrity": "sha512-tMPDYLMD+8+JyLPrsWft3NHBhK5YKOFOXzKMwpOKR5KvXOkd1silkArDwplmBUzN/eG/uRzWdtHZs9mHUQ5b9g==", "integrity": "sha512-xKmVK4ipgVQPJ/uyyrfH9LnaawERRWt8U2UZhdhGfzdL/QU/OpBjuhoIbFCv1Uy6qXV4nIiJ6Nw4MBC4HmXf1g==",
"dependencies": { "dependencies": {
"@bufbuild/protobuf": "^1.10.0", "isomorphic-ws": "^5.0.0",
"@connectrpc/connect": "^1.4.0", "normalize-path": "^3.0.0",
"@connectrpc/connect-web": "^1.4.0", "openapi-typescript-fetch": "^1.1.3",
"compare-versions": "^6.1.0", "path-browserify": "^1.0.1",
"openapi-fetch": "^0.9.7", "platform": "^1.3.6",
"platform": "^1.3.6" "ws": "^8.15.1"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
},
"optionalDependencies": {
"bufferutil": "^4.0.8",
"utf-8-validate": "^6.0.3"
} }
}, },
"node_modules/ee-first": { "node_modules/ee-first": {
@ -1219,6 +1195,14 @@
"node": ">=0.12.0" "node": ">=0.12.0"
} }
}, },
"node_modules/isomorphic-ws": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz",
"integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==",
"peerDependencies": {
"ws": "*"
}
},
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@ -1317,7 +1301,6 @@
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz",
"integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==",
"optional": true, "optional": true,
"peer": true,
"bin": { "bin": {
"node-gyp-build": "bin.js", "node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js", "node-gyp-build-optional": "optional.js",
@ -1400,7 +1383,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -1435,19 +1417,15 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/openapi-fetch": { "node_modules/openapi-typescript-fetch": {
"version": "0.9.8", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.9.8.tgz", "resolved": "https://registry.npmjs.org/openapi-typescript-fetch/-/openapi-typescript-fetch-1.1.3.tgz",
"integrity": "sha512-zM6elH0EZStD/gSiNlcPrzXcVQ/pZo3BDvC6CDwRDUt1dDzxlshpmQnpD6cZaJ39THaSmwVCxxRrPKNM1hHrDg==", "integrity": "sha512-smLZPck4OkKMNExcw8jMgrMOGgVGx2N/s6DbKL2ftNl77g5HfoGpZGFy79RBzU/EkaO0OZpwBnslfdBfh7ZcWg==",
"dependencies": { "engines": {
"openapi-typescript-helpers": "^0.0.8" "node": ">= 12.0.0",
"npm": ">= 7.0.0"
} }
}, },
"node_modules/openapi-typescript-helpers": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.8.tgz",
"integrity": "sha512-1eNjQtbfNi5Z/kFhagDIaIRj6qqDzhjNJKz8cmMW0CVdGwT6e1GLbAfgI0d28VTJa1A8jz82jm/4dG8qNoNS8g=="
},
"node_modules/parseurl": { "node_modules/parseurl": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@ -1456,6 +1434,11 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="
},
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "0.1.7", "version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
@ -2070,7 +2053,6 @@
"integrity": "sha512-xu9GQDeFp+eZ6LnCywXN/zBancWvOpUMzgjLPSjy4BRHSmTelvn2E0DG0o1sTiw5hkCKBHo8rwSKncfRfv2EEQ==", "integrity": "sha512-xu9GQDeFp+eZ6LnCywXN/zBancWvOpUMzgjLPSjy4BRHSmTelvn2E0DG0o1sTiw5hkCKBHo8rwSKncfRfv2EEQ==",
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"node-gyp-build": "^4.3.0" "node-gyp-build": "^4.3.0"
}, },
@ -2116,6 +2098,26 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1" "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
} }
}, },
"node_modules/ws": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/y18n": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@ -14,7 +14,7 @@
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"e2b": "^0.16.2-beta.47", "e2b": "^0.16.1",
"express": "^4.19.2", "express": "^4.19.2",
"rate-limiter-flexible": "^5.0.3", "rate-limiter-flexible": "^5.0.3",
"simple-git": "^3.25.0", "simple-git": "^3.25.0",

View File

@ -22,13 +22,13 @@ export class SecureGitClient {
try { try {
// Create a temporary directory // Create a temporary directory
tempDir = fs.mkdtempSync(path.posix.join(os.tmpdir(), 'git-push-')); tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'git-push-'));
console.log(`Temporary directory created: ${tempDir}`); console.log(`Temporary directory created: ${tempDir}`);
// Write files to the temporary directory // Write files to the temporary directory
console.log(`Writing ${fileData.length} files.`); console.log(`Writing ${fileData.length} files.`);
for (const { id, data } of fileData) { for (const { id, data } of fileData) {
const filePath = path.posix.join(tempDir, id); const filePath = path.join(tempDir, id);
const dirPath = path.dirname(filePath); const dirPath = path.dirname(filePath);
if (!fs.existsSync(dirPath)) { if (!fs.existsSync(dirPath)) {

View File

@ -1,67 +0,0 @@
import { Sandbox, ProcessHandle } from "e2b";
// Terminal class to manage a pseudo-terminal (PTY) in a sandbox environment
export class Terminal {
private pty: ProcessHandle | undefined; // Holds the PTY process handle
private sandbox: Sandbox; // Reference to the sandbox environment
// Constructor initializes the Terminal with a sandbox
constructor(sandbox: Sandbox) {
this.sandbox = sandbox;
}
// Initialize the terminal with specified rows, columns, and data handler
async init({
rows = 20,
cols = 80,
onData,
}: {
rows?: number;
cols?: number;
onData: (responseData: string) => void;
}): Promise<void> {
// Create a new PTY process
this.pty = await this.sandbox.pty.create({
rows,
cols,
timeout: 0,
onData: (data: Uint8Array) => {
onData(new TextDecoder().decode(data)); // Convert received data to string and pass to handler
},
});
}
// Send data to the terminal
async sendData(data: string) {
if (this.pty) {
await this.sandbox.pty.sendInput(this.pty.pid, new TextEncoder().encode(data));
} else {
console.log("Cannot send data because pty is not initialized.");
}
}
// Resize the terminal
async resize(size: { cols: number; rows: number }): Promise<void> {
if (this.pty) {
await this.sandbox.pty.resize(this.pty.pid, size);
} else {
console.log("Cannot resize terminal because pty is not initialized.");
}
}
// Close the terminal, killing the PTY process and stopping the input stream
async close(): Promise<void> {
if (this.pty) {
await this.pty.kill();
} else {
console.log("Cannot kill pty because it is not initialized.");
}
}
}
// Usage example:
// const terminal = new Terminal(sandbox);
// await terminal.init();
// terminal.sendData('ls -la');
// await terminal.resize({ cols: 100, rows: 30 });
// await terminal.close();

View File

@ -6,15 +6,10 @@ import { createServer } from "http";
import { Server } from "socket.io"; import { Server } from "socket.io";
import { DokkuClient } from "./DokkuClient"; import { DokkuClient } from "./DokkuClient";
import { SecureGitClient, FileData } from "./SecureGitClient"; import { SecureGitClient, FileData } from "./SecureGitClient";
import fs, { readFile } from "fs"; import fs from "fs";
import { z } from "zod"; import { z } from "zod";
import { import { User } from "./types";
TFile,
TFileData,
TFolder,
User
} from "./types";
import { import {
createFile, createFile,
deleteFile, deleteFile,
@ -25,11 +20,7 @@ import {
saveFile, saveFile,
} from "./fileoperations"; } from "./fileoperations";
import { LockManager } from "./utils"; import { LockManager } from "./utils";
import { Sandbox, Terminal, FilesystemManager } from "e2b";
import { Sandbox, Filesystem, FilesystemEvent, EntryInfo, WatchHandle } from "e2b";
import { Terminal } from "./Terminal"
import { import {
MAX_BODY_SIZE, MAX_BODY_SIZE,
createFileRL, createFileRL,
@ -39,21 +30,6 @@ import {
saveFileRL, saveFileRL,
} from "./ratelimit"; } from "./ratelimit";
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
// Do not exit the process
// You can add additional logging or recovery logic here
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// Do not exit the process
// You can also handle the rejected promise here if needed
});
// The amount of time in ms that a container will stay alive without a hearbeat.
const CONTAINER_TIMEOUT = 60_000;
dotenv.config(); dotenv.config();
const app: Express = express(); const app: Express = express();
@ -75,14 +51,14 @@ const terminals: Record<string, Terminal> = {};
const dirName = "/home/user"; const dirName = "/home/user";
const moveFile = async (filesystem: Filesystem, filePath: string, newFilePath: string) => { const moveFile = async (
try { filesystem: FilesystemManager,
const fileContents = await filesystem.read(filePath); filePath: string,
await filesystem.write(newFilePath, fileContents); newFilePath: string
) => {
const fileContents = await filesystem.readBytes(filePath);
await filesystem.writeBytes(newFilePath, fileContents);
await filesystem.remove(filePath); await filesystem.remove(filePath);
} catch (e) {
console.error(`Error moving file from ${filePath} to ${newFilePath}:`, e);
}
}; };
io.use(async (socket, next) => { io.use(async (socket, next) => {
@ -177,13 +153,11 @@ io.on("connection", async (socket) => {
} }
} }
const createdContainer = await lockManager.acquireLock(data.sandboxId, async () => { await lockManager.acquireLock(data.sandboxId, async () => {
try { try {
// Start a new container if the container doesn't exist or it timed out. if (!containers[data.sandboxId]) {
if (!containers[data.sandboxId] || !(await containers[data.sandboxId].isRunning())) { containers[data.sandboxId] = await Sandbox.create();
containers[data.sandboxId] = await Sandbox.create({ timeoutMs: CONTAINER_TIMEOUT });
console.log("Created container ", data.sandboxId); console.log("Created container ", data.sandboxId);
return true;
} }
} catch (e: any) { } catch (e: any) {
console.error(`Error creating container ${data.sandboxId}:`, e); console.error(`Error creating container ${data.sandboxId}:`, e);
@ -191,189 +165,25 @@ io.on("connection", async (socket) => {
} }
}); });
const sandboxFiles = await getSandboxFiles(data.sandboxId);
const projectDirectory = path.posix.join(dirName, "projects", data.sandboxId);
const containerFiles = containers[data.sandboxId].files;
const fileWatchers: WatchHandle[] = [];
// Change the owner of the project directory to user // Change the owner of the project directory to user
const fixPermissions = async (projectDirectory: string) => { const fixPermissions = async () => {
try { await containers[data.sandboxId].process.startAndWait(
await containers[data.sandboxId].commands.run( `sudo chown -R user "${path.join(dirName, "projects", data.sandboxId)}"`
`sudo chown -R user "${projectDirectory}"`
); );
} catch (e: any) {
console.log("Failed to fix permissions: " + e);
}
}; };
// Check if the given path is a directory const sandboxFiles = await getSandboxFiles(data.sandboxId);
const isDirectory = async (projectDirectory: string): Promise<boolean> => { sandboxFiles.fileData.forEach(async (file) => {
try { const filePath = path.join(dirName, file.id);
const result = await containers[data.sandboxId].commands.run( await containers[data.sandboxId].filesystem.makeDir(
`[ -d "${projectDirectory}" ] && echo "true" || echo "false"` path.dirname(filePath)
); );
return result.stdout.trim() === "true"; await containers[data.sandboxId].filesystem.write(filePath, file.data);
} catch (e: any) {
console.log("Failed to check if directory: " + e);
return false;
}
};
// Only continue to container setup if a new container was created
if (createdContainer) {
// Copy all files from the project to the container
const promises = sandboxFiles.fileData.map(async (file) => {
try {
const filePath = path.posix.join(dirName, file.id);
const parentDirectory = path.dirname(filePath);
if (!containerFiles.exists(parentDirectory)) {
await containerFiles.makeDir(parentDirectory);
}
await containerFiles.write(filePath, file.data);
} catch (e: any) {
console.log("Failed to create file: " + e);
}
}); });
await Promise.all(promises); fixPermissions();
// Make the logged in user the owner of all project files
fixPermissions(projectDirectory);
}
// Start filesystem watcher for the project directory
const watchDirectory = async (directory: string): Promise<WatchHandle | undefined> => {
try {
return await containerFiles.watch(directory, async (event: FilesystemEvent) => {
try {
function removeDirName(path : string, dirName : string) {
return path.startsWith(dirName) ? path.slice(dirName.length) : path;
}
// This is the absolute file path in the container
const containerFilePath = path.posix.join(directory, event.name);
// This is the file path relative to the home directory
const sandboxFilePath = removeDirName(containerFilePath, dirName + "/");
// This is the directory being watched relative to the home directory
const sandboxDirectory = removeDirName(directory, dirName + "/");
// Helper function to find a folder by id
function findFolderById(files: (TFolder | TFile)[], folderId : string) {
return files.find((file : TFolder | TFile) => file.type === "folder" && file.id === folderId);
}
// A new file or directory was created.
if (event.type === "create") {
const folder = findFolderById(sandboxFiles.files, sandboxDirectory) as TFolder;
const isDir = await isDirectory(containerFilePath);
const newItem = isDir
? { id: sandboxFilePath, name: event.name, type: "folder", children: [] } as TFolder
: { id: sandboxFilePath, name: event.name, type: "file" } as TFile;
if (folder) {
// If the folder exists, add the new item (file/folder) as a child
folder.children.push(newItem);
} else {
// If folder doesn't exist, add the new item to the root
sandboxFiles.files.push(newItem);
}
if (!isDir) {
const fileData = await containers[data.sandboxId].files.read(containerFilePath);
const fileContents = typeof fileData === "string" ? fileData : "";
sandboxFiles.fileData.push({ id: sandboxFilePath, data: fileContents });
}
console.log(`Create ${sandboxFilePath}`);
}
// A file or directory was removed or renamed.
else if (event.type === "remove" || event.type == "rename") {
const folder = findFolderById(sandboxFiles.files, sandboxDirectory) as TFolder;
const isDir = await isDirectory(containerFilePath);
const isFileMatch = (file: TFolder | TFile | TFileData) => file.id === sandboxFilePath || file.id.startsWith(containerFilePath + '/');
if (folder) {
// Remove item from its parent folder
folder.children = folder.children.filter((file: TFolder | TFile) => !isFileMatch(file));
} else {
// Remove from the root if it's not inside a folder
sandboxFiles.files = sandboxFiles.files.filter((file: TFolder | TFile) => !isFileMatch(file));
}
// Also remove any corresponding file data
sandboxFiles.fileData = sandboxFiles.fileData.filter((file: TFileData) => !isFileMatch(file));
console.log(`Removed: ${sandboxFilePath}`);
}
// The contents of a file were changed.
else if (event.type === "write") {
const folder = findFolderById(sandboxFiles.files, sandboxDirectory) as TFolder;
const fileToWrite = sandboxFiles.fileData.find(file => file.id === sandboxFilePath);
if (fileToWrite) {
fileToWrite.data = await containers[data.sandboxId].files.read(containerFilePath);
console.log(`Write to ${sandboxFilePath}`);
} else {
// If the file is part of a folder structure, locate it and update its data
const fileInFolder = folder?.children.find(file => file.id === sandboxFilePath);
if (fileInFolder) {
const fileData = await containers[data.sandboxId].files.read(containerFilePath);
const fileContents = typeof fileData === "string" ? fileData : "";
sandboxFiles.fileData.push({ id: sandboxFilePath, data: fileContents });
console.log(`Write to ${sandboxFilePath}`);
}
}
}
// Tell the client to reload the file list
socket.emit("loaded", sandboxFiles.files);
} catch (error) {
console.error(`Error handling ${event.type} event for ${event.name}:`, error);
}
}, { "timeout": 0 } )
} catch (error) {
console.error(`Error watching filesystem:`, error);
}
};
// Watch the project directory
const handle = await watchDirectory(projectDirectory);
// Keep track of watch handlers to close later
if (handle) fileWatchers.push(handle);
// Watch all subdirectories of the project directory, but not deeper
// This also means directories created after the container is created won't be watched
const dirContent = await containerFiles.list(projectDirectory);
await Promise.all(dirContent.map(async (item : EntryInfo) => {
if (item.type === "dir") {
console.log("Watching " + item.path);
// Keep track of watch handlers to close later
const handle = await watchDirectory(item.path);
if (handle) fileWatchers.push(handle);
}
}))
socket.emit("loaded", sandboxFiles.files); socket.emit("loaded", sandboxFiles.files);
socket.on("heartbeat", async () => {
try {
// This keeps the container alive for another CONTAINER_TIMEOUT seconds.
// The E2B docs are unclear, but the timeout is relative to the time of this method call.
await containers[data.sandboxId].setTimeout(CONTAINER_TIMEOUT);
} catch (e: any) {
console.error("Error setting timeout:", e);
io.emit("error", `Error: set timeout. ${e.message ?? e}`);
}
});
socket.on("getFile", (fileId: string, callback) => { socket.on("getFile", (fileId: string, callback) => {
console.log(fileId); console.log(fileId);
try { try {
@ -421,11 +231,11 @@ io.on("connection", async (socket) => {
if (!file) return; if (!file) return;
file.data = body; file.data = body;
await containers[data.sandboxId].files.write( await containers[data.sandboxId].filesystem.write(
path.posix.join(dirName, file.id), path.join(dirName, file.id),
body body
); );
fixPermissions(projectDirectory); fixPermissions();
} catch (e: any) { } catch (e: any) {
console.error("Error saving file:", e); console.error("Error saving file:", e);
io.emit("error", `Error: file saving. ${e.message ?? e}`); io.emit("error", `Error: file saving. ${e.message ?? e}`);
@ -443,11 +253,11 @@ io.on("connection", async (socket) => {
const newFileId = folderId + "/" + parts.pop(); const newFileId = folderId + "/" + parts.pop();
await moveFile( await moveFile(
containers[data.sandboxId].files, containers[data.sandboxId].filesystem,
path.posix.join(dirName, fileId), path.join(dirName, fileId),
path.posix.join(dirName, newFileId) path.join(dirName, newFileId)
); );
fixPermissions(projectDirectory); fixPermissions();
file.id = newFileId; file.id = newFileId;
@ -536,11 +346,11 @@ io.on("connection", async (socket) => {
const id = `projects/${data.sandboxId}/${name}`; const id = `projects/${data.sandboxId}/${name}`;
await containers[data.sandboxId].files.write( await containers[data.sandboxId].filesystem.write(
path.posix.join(dirName, id), path.join(dirName, id),
"" ""
); );
fixPermissions(projectDirectory); fixPermissions();
sandboxFiles.files.push({ sandboxFiles.files.push({
id, id,
@ -573,8 +383,8 @@ io.on("connection", async (socket) => {
const id = `projects/${data.sandboxId}/${name}`; const id = `projects/${data.sandboxId}/${name}`;
await containers[data.sandboxId].files.makeDir( await containers[data.sandboxId].filesystem.makeDir(
path.posix.join(dirName, id) path.join(dirName, id)
); );
callback(); callback();
@ -602,11 +412,11 @@ io.on("connection", async (socket) => {
parts.slice(0, parts.length - 1).join("/") + "/" + newName; parts.slice(0, parts.length - 1).join("/") + "/" + newName;
await moveFile( await moveFile(
containers[data.sandboxId].files, containers[data.sandboxId].filesystem,
path.posix.join(dirName, fileId), path.join(dirName, fileId),
path.posix.join(dirName, newFileId) path.join(dirName, newFileId)
); );
fixPermissions(projectDirectory); fixPermissions();
await renameFile(fileId, newFileId, file.data); await renameFile(fileId, newFileId, file.data);
} catch (e: any) { } catch (e: any) {
console.error("Error renaming folder:", e); console.error("Error renaming folder:", e);
@ -625,8 +435,8 @@ io.on("connection", async (socket) => {
const file = sandboxFiles.fileData.find((f) => f.id === fileId); const file = sandboxFiles.fileData.find((f) => f.id === fileId);
if (!file) return; if (!file) return;
await containers[data.sandboxId].files.remove( await containers[data.sandboxId].filesystem.remove(
path.posix.join(dirName, fileId) path.join(dirName, fileId)
); );
sandboxFiles.fileData = sandboxFiles.fileData.filter( sandboxFiles.fileData = sandboxFiles.fileData.filter(
(f) => f.id !== fileId (f) => f.id !== fileId
@ -652,8 +462,8 @@ io.on("connection", async (socket) => {
await Promise.all( await Promise.all(
files.map(async (file) => { files.map(async (file) => {
await containers[data.sandboxId].files.remove( await containers[data.sandboxId].filesystem.remove(
path.posix.join(dirName, file) path.join(dirName, file)
); );
sandboxFiles.fileData = sandboxFiles.fileData.filter( sandboxFiles.fileData = sandboxFiles.fileData.filter(
@ -675,49 +485,41 @@ io.on("connection", async (socket) => {
socket.on("createTerminal", async (id: string, callback) => { socket.on("createTerminal", async (id: string, callback) => {
try { try {
// Note: The number of terminals per window is limited on the frontend, but not backend if (terminals[id] || Object.keys(terminals).length >= 4) {
if (terminals[id]) {
return; return;
} }
await lockManager.acquireLock(data.sandboxId, async () => { await lockManager.acquireLock(data.sandboxId, async () => {
try { try {
terminals[id] = new Terminal(containers[data.sandboxId]) terminals[id] = await containers[data.sandboxId].terminal.start({
await terminals[id].init({ onData: (responseData: string) => {
onData: (responseString: string) => { io.emit("terminalResponse", { id, data: responseData });
io.emit("terminalResponse", { id, data: responseString });
function extractPortNumber(inputString: string) { function extractPortNumber(inputString: string) {
// Remove ANSI escape codes // Remove ANSI escape codes
const cleanedString = inputString.replace(/\x1B\[[0-9;]*m/g, ''); const cleanedString = inputString.replace(/\x1B\[[0-9;]*m/g, '');
// Regular expression to match port number // Regular expression to match port number
const regex = /http:\/\/localhost:(\d+)/; const regex = /http:\/\/localhost:(\d+)/;
// If a match is found, return the port number // If a match is found, return the port number
const match = cleanedString.match(regex); const match = cleanedString.match(regex);
return match ? match[1] : null; return match ? match[1] : null;
} }
const port = parseInt(extractPortNumber(responseString) ?? ""); const port = parseInt(extractPortNumber(responseData) ?? "");
if (port) { if (port) {
io.emit( io.emit(
"previewURL", "previewURL",
"https://" + containers[data.sandboxId].getHost(port) "https://" + containers[data.sandboxId].getHostname(port)
); );
} }
}, },
cols: 80, size: { cols: 80, rows: 20 },
rows: 20, onExit: () => console.log("Terminal exited", id),
//onExit: () => console.log("Terminal exited", id),
}); });
await terminals[id].sendData(
const defaultDirectory = path.posix.join(dirName, "projects", data.sandboxId); `cd "${path.join(dirName, "projects", data.sandboxId)}"\r`
const defaultCommands = [ );
`cd "${defaultDirectory}"`, await terminals[id].sendData("export PS1='user> '\rclear\r");
"export PS1='user> '",
"clear"
]
for (const command of defaultCommands) await terminals[id].sendData(command + "\r");
console.log("Created terminal", id); console.log("Created terminal", id);
} catch (e: any) { } catch (e: any) {
console.error(`Error creating terminal ${id}:`, e); console.error(`Error creating terminal ${id}:`, e);
@ -746,13 +548,13 @@ io.on("connection", async (socket) => {
} }
); );
socket.on("terminalData", async (id: string, data: string) => { socket.on("terminalData", (id: string, data: string) => {
try { try {
if (!terminals[id]) { if (!terminals[id]) {
return; return;
} }
await terminals[id].sendData(data); terminals[id].sendData(data);
} catch (e: any) { } catch (e: any) {
console.error("Error writing to terminal:", e); console.error("Error writing to terminal:", e);
io.emit("error", `Error: writing to terminal. ${e.message ?? e}`); io.emit("error", `Error: writing to terminal. ${e.message ?? e}`);
@ -765,7 +567,7 @@ io.on("connection", async (socket) => {
return; return;
} }
await terminals[id].close(); await terminals[id].kill();
delete terminals[id]; delete terminals[id];
callback(); callback();
@ -801,7 +603,7 @@ io.on("connection", async (socket) => {
// Generate code from cloudflare workers AI // Generate code from cloudflare workers AI
const generateCodePromise = fetch( const generateCodePromise = fetch(
`${process.env.AI_WORKER_URL}/api?fileName=${encodeURIComponent(fileName)}&code=${encodeURIComponent(code)}&line=${encodeURIComponent(line)}&instructions=${encodeURIComponent(instructions)}`, `${process.env.AI_WORKER_URL}/api?fileName=${fileName}&code=${code}&line=${line}&instructions=${instructions}`,
{ {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -815,28 +617,12 @@ io.on("connection", async (socket) => {
generateCodePromise, generateCodePromise,
]); ]);
if (!generateCodeResponse.ok) { const json = await generateCodeResponse.json();
throw new Error(`HTTP error! status: ${generateCodeResponse.status}`);
}
const reader = generateCodeResponse.body?.getReader(); callback({ response: json.response, success: true });
const decoder = new TextDecoder();
let result = '';
if (reader) {
while (true) {
const { done, value } = await reader.read();
if (done) break;
result += decoder.decode(value, { stream: true });
}
}
// The result should now contain only the modified code
callback({ response: result.trim(), success: true });
} catch (e: any) { } catch (e: any) {
console.error("Error generating code:", e); console.error("Error generating code:", e);
io.emit("error", `Error: code generation. ${e.message ?? e}`); io.emit("error", `Error: code generation. ${e.message ?? e}`);
callback({ response: "Error generating code. Please try again.", success: false });
} }
} }
); );
@ -847,12 +633,26 @@ io.on("connection", async (socket) => {
connections[data.sandboxId]--; connections[data.sandboxId]--;
} }
// Stop watching file changes in the container
Promise.all(fileWatchers.map(async (handle : WatchHandle) => {
await handle.close();
}));
if (data.isOwner && connections[data.sandboxId] <= 0) { if (data.isOwner && connections[data.sandboxId] <= 0) {
await Promise.all(
Object.entries(terminals).map(async ([key, terminal]) => {
await terminal.kill();
delete terminals[key];
})
);
await lockManager.acquireLock(data.sandboxId, async () => {
try {
if (containers[data.sandboxId]) {
await containers[data.sandboxId].close();
delete containers[data.sandboxId];
console.log("Closed container", data.sandboxId);
}
} catch (error) {
console.error("Error closing container ", data.sandboxId, error);
}
});
socket.broadcast.emit( socket.broadcast.emit(
"disableAccess", "disableAccess",
"The sandbox owner has disconnected." "The sandbox owner has disconnected."

View File

@ -8,7 +8,6 @@
"name": "storage", "name": "storage",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"p-limit": "^6.1.0",
"zod": "^3.23.4" "zod": "^3.23.4"
}, },
"devDependencies": { "devDependencies": {
@ -895,21 +894,6 @@
"url": "https://opencollective.com/vitest" "url": "https://opencollective.com/vitest"
} }
}, },
"node_modules/@vitest/runner/node_modules/p-limit": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz",
"integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==",
"dev": true,
"dependencies": {
"yocto-queue": "^1.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@vitest/snapshot": { "node_modules/@vitest/snapshot": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.0.tgz", "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.0.tgz",
@ -1782,11 +1766,12 @@
} }
}, },
"node_modules/p-limit": { "node_modules/p-limit": {
"version": "6.1.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.1.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz",
"integrity": "sha512-H0jc0q1vOzlEk0TqAKXKZxdl7kX3OFUzCnNVUnq5Pc3DGo0kpeaMuPqxQn235HibwBEb0/pm9dgKTjXy66fBkg==", "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==",
"dev": true,
"dependencies": { "dependencies": {
"yocto-queue": "^1.1.1" "yocto-queue": "^1.0.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@ -2985,9 +2970,10 @@
"dev": true "dev": true
}, },
"node_modules/yocto-queue": { "node_modules/yocto-queue": {
"version": "1.1.1", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
"integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==",
"dev": true,
"engines": { "engines": {
"node": ">=12.20" "node": ">=12.20"
}, },

View File

@ -17,7 +17,6 @@
"wrangler": "^3.0.0" "wrangler": "^3.0.0"
}, },
"dependencies": { "dependencies": {
"p-limit": "^6.1.0",
"zod": "^3.23.4" "zod": "^3.23.4"
} }
} }

View File

@ -1,9 +1,7 @@
import { z } from "zod" import { z } from "zod"
import pLimit from 'p-limit';
export interface Env { export interface Env {
R2: R2Bucket R2: R2Bucket
Templates: R2Bucket
KEY: string KEY: string
} }
@ -146,18 +144,17 @@ export default {
console.log(`Copying template: ${type}`); console.log(`Copying template: ${type}`);
// List all objects under the directory const templateDirectory = `templates/${type}`;
const { objects } = await env.Templates.list({ prefix: type });
// Copy each object to the new directory with a 5 concurrency limit // List all objects under the directory
const limit = pLimit(5); const { objects } = await env.R2.list({ prefix: templateDirectory });
await Promise.all(objects.map(({ key }) =>
limit(async () => { // Copy each object to the new directory
const destinationKey = key.replace(type, `projects/${sandboxId}`); for (const { key } of objects) {
const fileBody = await env.Templates.get(key).then(res => res?.body ?? ""); const destinationKey = key.replace(templateDirectory, `projects/${sandboxId}`);
const fileBody = await env.R2.get(key).then(res => res?.body ?? "");
await env.R2.put(destinationKey, fileBody); await env.R2.put(destinationKey, fileBody);
}) }
));
return success return success
} else { } else {

View File

@ -6,7 +6,6 @@ import { notFound, redirect } from "next/navigation"
import Loading from "@/components/editor/loading" import Loading from "@/components/editor/loading"
import dynamic from "next/dynamic" import dynamic from "next/dynamic"
import fs from "fs" import fs from "fs"
import { TerminalProvider } from "@/context/TerminalContext"
export const revalidate = 0 export const revalidate = 0
@ -88,10 +87,8 @@ export default async function CodePage({ params }: { params: { id: string } }) {
} }
return ( return (
<>
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background"> <div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
<Room id={sandboxId}> <Room id={sandboxId}>
<TerminalProvider>
<Navbar userData={userData} sandboxData={sandboxData} shared={shared} /> <Navbar userData={userData} sandboxData={sandboxData} shared={shared} />
<div className="w-screen flex grow"> <div className="w-screen flex grow">
<CodeEditor <CodeEditor
@ -99,9 +96,7 @@ export default async function CodePage({ params }: { params: { id: string } }) {
sandboxData={sandboxData} sandboxData={sandboxData}
/> />
</div> </div>
</TerminalProvider>
</Room> </Room>
</div> </div>
</>
) )
} }

View File

@ -6,6 +6,7 @@ import { ThemeProvider } from "@/components/layout/themeProvider"
import { ClerkProvider } from "@clerk/nextjs" import { ClerkProvider } from "@clerk/nextjs"
import { Toaster } from "@/components/ui/sonner" import { Toaster } from "@/components/ui/sonner"
import { Analytics } from "@vercel/analytics/react" import { Analytics } from "@vercel/analytics/react"
import { TerminalProvider } from '@/context/TerminalContext';
import { PreviewProvider } from "@/context/PreviewContext"; import { PreviewProvider } from "@/context/PreviewContext";
import { SocketProvider } from '@/context/SocketContext' import { SocketProvider } from '@/context/SocketContext'
@ -31,7 +32,9 @@ export default function RootLayout({
> >
<SocketProvider> <SocketProvider>
<PreviewProvider> <PreviewProvider>
<TerminalProvider>
{children} {children}
</TerminalProvider>
</PreviewProvider> </PreviewProvider>
</SocketProvider> </SocketProvider>
<Analytics /> <Analytics />

View File

@ -51,7 +51,7 @@ export default function Dashboard({
useEffect(() => { // update the dashboard to show a new project useEffect(() => { // update the dashboard to show a new project
router.refresh() router.refresh()
}, []) }, [sandboxes])
return ( return (
<> <>

View File

@ -36,7 +36,43 @@ import { createSandbox } from "@/lib/actions"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { Loader2 } from "lucide-react" import { Loader2 } from "lucide-react"
import { Button } from "../ui/button" import { Button } from "../ui/button"
import { projectTemplates } from "@/lib/data"
const data: {
id: string
name: string
icon: string
description: string
disabled: boolean
}[] = [
{
id: "reactjs",
name: "React",
icon: "/project-icons/react.svg",
description: "A JavaScript library for building user interfaces",
disabled: false,
},
{
id: "vanillajs",
name: "HTML/JS",
icon: "/project-icons/more.svg",
description: "More coming soon, feel free to contribute on GitHub",
disabled: false,
},
{
id: "nextjs",
name: "NextJS",
icon: "/project-icons/node.svg",
description: "A JavaScript runtime built on the V8 JavaScript engine",
disabled: false,
},
{
id: "streamlit",
name: "Streamlit",
icon: "/project-icons/python.svg",
description: "A JavaScript runtime built on the V8 JavaScript engine",
disabled: false,
}
]
const formSchema = z.object({ const formSchema = z.object({
name: z name: z
@ -57,7 +93,7 @@ export default function NewProjectModal({
open: boolean open: boolean
setOpen: (open: boolean) => void setOpen: (open: boolean) => void
}) { }) {
const [selected, setSelected] = useState("reactjs") const [selected, setSelected] = useState("react")
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const router = useRouter() const router = useRouter()
@ -88,12 +124,12 @@ export default function NewProjectModal({
if (!loading) setOpen(open) if (!loading) setOpen(open)
}} }}
> >
<DialogContent className="max-h-[95vh] overflow-y-auto"> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Create A Sandbox</DialogTitle> <DialogTitle>Create A Sandbox</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="grid grid-cols-2 w-full gap-2 mt-2"> <div className="grid grid-cols-2 w-full gap-2 mt-2">
{projectTemplates.map((item) => ( {data.map((item) => (
<button <button
disabled={item.disabled || loading} disabled={item.disabled || loading}
key={item.id} key={item.id}

View File

@ -8,7 +8,6 @@ import { Clock, Globe, Lock } from "lucide-react"
import { Sandbox } from "@/lib/types" import { Sandbox } from "@/lib/types"
import { Card } from "@/components/ui/card" import { Card } from "@/components/ui/card"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { projectTemplates } from "@/lib/data"
export default function ProjectCard({ export default function ProjectCard({
children, children,
@ -44,9 +43,7 @@ export default function ProjectCard({
setDate(`${Math.floor(diffInMinutes / 1440)}d ago`) setDate(`${Math.floor(diffInMinutes / 1440)}d ago`)
} }
}, [sandbox]) }, [sandbox])
const projectIcon =
projectTemplates.find((p) => p.id === sandbox.type)?.icon ??
"/project-icons/node.svg"
return ( return (
<Card <Card
tabIndex={0} tabIndex={0}
@ -68,7 +65,16 @@ export default function ProjectCard({
</AnimatePresence> </AnimatePresence>
<div className="space-x-2 flex items-center justify-start w-full z-10"> <div className="space-x-2 flex items-center justify-start w-full z-10">
<Image alt="" src={projectIcon} width={20} height={20} /> <Image
alt=""
src={
sandbox.type === "react"
? "/project-icons/react.svg"
: "/project-icons/node.svg"
}
width={20}
height={20}
/>
<div className="font-medium static whitespace-nowrap w-full text-ellipsis overflow-hidden"> <div className="font-medium static whitespace-nowrap w-full text-ellipsis overflow-hidden">
{sandbox.name} {sandbox.name}
</div> </div>

View File

@ -1,36 +0,0 @@
import React from 'react';
import { Button } from '../../ui/button';
import { Send, StopCircle } from 'lucide-react';
interface ChatInputProps {
input: string;
setInput: (input: string) => void;
isGenerating: boolean;
handleSend: () => void;
handleStopGeneration: () => void;
}
export default function ChatInput({ input, setInput, isGenerating, handleSend, handleStopGeneration }: ChatInputProps) {
return (
<div className="flex space-x-2 min-w-0">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && !isGenerating && handleSend()}
className="flex-grow p-2 border rounded-lg min-w-0 bg-input"
placeholder="Type your message..."
disabled={isGenerating}
/>
{isGenerating ? (
<Button onClick={handleStopGeneration} variant="destructive" size="icon" className="h-10 w-10">
<StopCircle className="w-4 h-4" />
</Button>
) : (
<Button onClick={handleSend} disabled={isGenerating} size="icon" className="h-10 w-10">
<Send className="w-4 h-4" />
</Button>
)}
</div>
);
}

View File

@ -1,201 +0,0 @@
import React, { useState } from 'react';
import { Button } from '../../ui/button';
import { ChevronUp, ChevronDown, Copy, Check, CornerUpLeft } from 'lucide-react';
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import remarkGfm from 'remark-gfm';
import { copyToClipboard, stringifyContent } from './lib/chatUtils';
interface MessageProps {
message: {
role: 'user' | 'assistant';
content: string;
context?: string;
};
setContext: (context: string | null) => void;
setIsContextExpanded: (isExpanded: boolean) => void;
}
export default function ChatMessage({ message, setContext, setIsContextExpanded }: MessageProps) {
const [expandedMessageIndex, setExpandedMessageIndex] = useState<number | null>(null);
const [copiedText, setCopiedText] = useState<string | null>(null);
const renderCopyButton = (text: any) => (
<Button
onClick={() => copyToClipboard(stringifyContent(text), setCopiedText)}
size="sm"
variant="ghost"
className="p-1 h-6"
>
{copiedText === stringifyContent(text) ? (
<Check className="w-4 h-4 text-green-500" />
) : (
<Copy className="w-4 h-4" />
)}
</Button>
);
const askAboutCode = (code: any) => {
const contextString = stringifyContent(code);
setContext(`Regarding this code:\n${contextString}`);
setIsContextExpanded(false);
};
const renderMarkdownElement = (props: any) => {
const { node, children } = props;
const content = stringifyContent(children);
return (
<div className="relative group">
<div className="absolute top-0 right-0 flex opacity-0 group-hover:opacity-30 transition-opacity">
{renderCopyButton(content)}
<Button
onClick={() => askAboutCode(content)}
size="sm"
variant="ghost"
className="p-1 h-6"
>
<CornerUpLeft className="w-4 h-4" />
</Button>
</div>
{React.createElement(node.tagName, {
...props,
className: `${props.className || ''} hover:bg-transparent rounded p-1 transition-colors`
}, children)}
</div>
);
};
return (
<div className="text-left relative">
<div className={`relative p-2 rounded-lg ${
message.role === 'user'
? 'bg-[#262626] text-white'
: 'bg-transparent text-white'
} max-w-full`}>
{message.role === 'user' && (
<div className="absolute top-0 right-0 flex opacity-0 group-hover:opacity-30 transition-opacity">
{renderCopyButton(message.content)}
<Button
onClick={() => askAboutCode(message.content)}
size="sm"
variant="ghost"
className="p-1 h-6"
>
<CornerUpLeft className="w-4 h-4" />
</Button>
</div>
)}
{message.context && (
<div className="mb-2 bg-input rounded-lg">
<div
className="flex justify-between items-center cursor-pointer"
onClick={() => setExpandedMessageIndex(expandedMessageIndex === 0 ? null : 0)}
>
<span className="text-sm text-gray-300">
Context
</span>
{expandedMessageIndex === 0 ? (
<ChevronUp size={16} />
) : (
<ChevronDown size={16} />
)}
</div>
{expandedMessageIndex === 0 && (
<div className="relative">
<div className="absolute top-0 right-0 flex p-1">
{renderCopyButton(message.context.replace(/^Regarding this code:\n/, ''))}
</div>
{(() => {
const code = message.context.replace(/^Regarding this code:\n/, '');
const match = /language-(\w+)/.exec(code);
const language = match ? match[1] : 'typescript';
return (
<div className="pt-6">
<textarea
value={code}
onChange={(e) => {
const updatedContext = `Regarding this code:\n${e.target.value}`;
setContext(updatedContext);
}}
className="w-full p-2 bg-[#1e1e1e] text-white font-mono text-sm rounded"
rows={code.split('\n').length}
style={{
resize: 'vertical',
minHeight: '100px',
maxHeight: '400px',
}}
/>
</div>
);
})()}
</div>
)}
</div>
)}
{message.role === 'assistant' ? (
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
code({node, className, children, ...props}) {
const match = /language-(\w+)/.exec(className || '');
return match ? (
<div className="relative border border-input rounded-md my-4">
<div className="absolute top-0 left-0 px-2 py-1 text-xs font-semibold text-gray-200 bg-#1e1e1e rounded-tl">
{match[1]}
</div>
<div className="absolute top-0 right-0 flex">
{renderCopyButton(children)}
<Button
onClick={() => askAboutCode(children)}
size="sm"
variant="ghost"
className="p-1 h-6"
>
<CornerUpLeft className="w-4 h-4" />
</Button>
</div>
<div className="pt-6">
<SyntaxHighlighter
style={vscDarkPlus as any}
language={match[1]}
PreTag="div"
customStyle={{
margin: 0,
padding: '0.5rem',
fontSize: '0.875rem',
}}
>
{stringifyContent(children)}
</SyntaxHighlighter>
</div>
</div>
) : (
<code className={className} {...props}>
{children}
</code>
);
},
p: renderMarkdownElement,
h1: renderMarkdownElement,
h2: renderMarkdownElement,
h3: renderMarkdownElement,
h4: renderMarkdownElement,
h5: renderMarkdownElement,
h6: renderMarkdownElement,
ul: (props) => <ul className="list-disc pl-6 mb-4 space-y-2">{props.children}</ul>,
ol: (props) => <ol className="list-decimal pl-6 mb-4 space-y-2">{props.children}</ol>,
}}
>
{message.content}
</ReactMarkdown>
) : (
<div className="whitespace-pre-wrap group">
{message.content}
</div>
)}
</div>
</div>
);
}

View File

@ -1,48 +0,0 @@
import React from 'react';
import { ChevronUp, ChevronDown, X } from 'lucide-react';
interface ContextDisplayProps {
context: string | null;
isContextExpanded: boolean;
setIsContextExpanded: (isExpanded: boolean) => void;
setContext: (context: string | null) => void;
}
export default function ContextDisplay({ context, isContextExpanded, setIsContextExpanded, setContext }: ContextDisplayProps) {
if (!context) return null;
return (
<div className="mb-2 bg-input p-2 rounded-lg">
<div className="flex justify-between items-center">
<div
className="flex-grow cursor-pointer"
onClick={() => setIsContextExpanded(!isContextExpanded)}
>
<span className="text-sm text-gray-300">
Context
</span>
</div>
<div className="flex items-center">
{isContextExpanded ? (
<ChevronUp size={16} className="cursor-pointer" onClick={() => setIsContextExpanded(false)} />
) : (
<ChevronDown size={16} className="cursor-pointer" onClick={() => setIsContextExpanded(true)} />
)}
<X
size={16}
className="ml-2 cursor-pointer text-gray-400 hover:text-gray-200"
onClick={() => setContext(null)}
/>
</div>
</div>
{isContextExpanded && (
<textarea
value={context.replace(/^Regarding this code:\n/, '')}
onChange={(e) => setContext(`Regarding this code:\n${e.target.value}`)}
className="w-full mt-2 p-2 bg-#1e1e1e text-white rounded"
rows={5}
/>
)}
</div>
);
}

View File

@ -1,84 +0,0 @@
import React, { useState, useEffect, useRef } from 'react';
import LoadingDots from '../../ui/LoadingDots';
import ChatMessage from './ChatMessage';
import ChatInput from './ChatInput';
import ContextDisplay from './ContextDisplay';
import { handleSend, handleStopGeneration } from './lib/chatUtils';
import { X } from 'lucide-react';
interface Message {
role: 'user' | 'assistant';
content: string;
context?: string;
}
export default function AIChat({ activeFileContent, activeFileName, onClose }: { activeFileContent: string, activeFileName: string, onClose: () => void }) {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [isGenerating, setIsGenerating] = useState(false);
const chatContainerRef = useRef<HTMLDivElement>(null);
const abortControllerRef = useRef<AbortController | null>(null);
const [context, setContext] = useState<string | null>(null);
const [isContextExpanded, setIsContextExpanded] = useState(false);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
scrollToBottom();
}, [messages]);
const scrollToBottom = () => {
if (chatContainerRef.current) {
setTimeout(() => {
chatContainerRef.current?.scrollTo({
top: chatContainerRef.current.scrollHeight,
behavior: 'smooth'
});
}, 100);
}
};
return (
<div className="flex flex-col h-screen w-full">
<div className="flex justify-between items-center p-2 border-b">
<span className="text-muted-foreground/50 font-medium">CHAT</span>
<div className="flex items-center h-full">
<span className="text-muted-foreground/50 font-medium">{activeFileName}</span>
<div className="mx-2 h-full w-px bg-muted-foreground/20"></div>
<button
onClick={onClose}
className="text-muted-foreground/50 hover:text-muted-foreground focus:outline-none"
aria-label="Close AI Chat"
>
<X size={18} />
</button>
</div>
</div>
<div ref={chatContainerRef} className="flex-grow overflow-y-auto p-4 space-y-4">
{messages.map((message, messageIndex) => (
<ChatMessage
key={messageIndex}
message={message}
setContext={setContext}
setIsContextExpanded={setIsContextExpanded}
/>
))}
{isLoading && <LoadingDots />}
</div>
<div className="p-4 border-t mb-14">
<ContextDisplay
context={context}
isContextExpanded={isContextExpanded}
setIsContextExpanded={setIsContextExpanded}
setContext={setContext}
/>
<ChatInput
input={input}
setInput={setInput}
isGenerating={isGenerating}
handleSend={() => handleSend(input, context, messages, setMessages, setInput, setIsContextExpanded, setIsGenerating, setIsLoading, abortControllerRef, activeFileContent)}
handleStopGeneration={() => handleStopGeneration(abortControllerRef)}
/>
</div>
</div>
);
}

View File

@ -1,162 +0,0 @@
import React from 'react';
export const stringifyContent = (content: any, seen = new WeakSet()): string => {
if (typeof content === 'string') {
return content;
}
if (content === null) {
return 'null';
}
if (content === undefined) {
return 'undefined';
}
if (typeof content === 'number' || typeof content === 'boolean') {
return content.toString();
}
if (typeof content === 'function') {
return content.toString();
}
if (typeof content === 'symbol') {
return content.toString();
}
if (typeof content === 'bigint') {
return content.toString() + 'n';
}
if (React.isValidElement(content)) {
return React.Children.toArray((content as React.ReactElement).props.children)
.map(child => stringifyContent(child, seen))
.join('');
}
if (Array.isArray(content)) {
return '[' + content.map(item => stringifyContent(item, seen)).join(', ') + ']';
}
if (typeof content === 'object') {
if (seen.has(content)) {
return '[Circular]';
}
seen.add(content);
try {
const pairs = Object.entries(content).map(
([key, value]) => `${key}: ${stringifyContent(value, seen)}`
);
return '{' + pairs.join(', ') + '}';
} catch (error) {
return Object.prototype.toString.call(content);
}
}
return String(content);
};
export const copyToClipboard = (text: string, setCopiedText: (text: string | null) => void) => {
navigator.clipboard.writeText(text).then(() => {
setCopiedText(text);
setTimeout(() => setCopiedText(null), 2000);
});
};
export const handleSend = async (
input: string,
context: string | null,
messages: any[],
setMessages: React.Dispatch<React.SetStateAction<any[]>>,
setInput: React.Dispatch<React.SetStateAction<string>>,
setIsContextExpanded: React.Dispatch<React.SetStateAction<boolean>>,
setIsGenerating: React.Dispatch<React.SetStateAction<boolean>>,
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>,
abortControllerRef: React.MutableRefObject<AbortController | null>,
activeFileContent: string
) => {
if (input.trim() === '' && !context) return;
const newMessage = {
role: 'user' as const,
content: input,
context: context || undefined
};
const updatedMessages = [...messages, newMessage];
setMessages(updatedMessages);
setInput('');
setIsContextExpanded(false);
setIsGenerating(true);
setIsLoading(true);
abortControllerRef.current = new AbortController();
try {
const anthropicMessages = updatedMessages.map(msg => ({
role: msg.role === 'user' ? 'human' : 'assistant',
content: msg.content
}));
const response = await fetch(`${process.env.NEXT_PUBLIC_AI_WORKER_URL}/api`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
messages: anthropicMessages,
context: context || undefined,
activeFileContent: activeFileContent,
}),
signal: abortControllerRef.current.signal,
});
if (!response.ok) {
throw new Error('Failed to get AI response');
}
const reader = response.body?.getReader();
const decoder = new TextDecoder();
const assistantMessage = { role: 'assistant' as const, content: '' };
setMessages([...updatedMessages, assistantMessage]);
setIsLoading(false);
let buffer = '';
const updateInterval = 100;
let lastUpdateTime = Date.now();
if (reader) {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const currentTime = Date.now();
if (currentTime - lastUpdateTime > updateInterval) {
setMessages(prev => {
const updatedMessages = [...prev];
const lastMessage = updatedMessages[updatedMessages.length - 1];
lastMessage.content = buffer;
return updatedMessages;
});
lastUpdateTime = currentTime;
}
}
setMessages(prev => {
const updatedMessages = [...prev];
const lastMessage = updatedMessages[updatedMessages.length - 1];
lastMessage.content = buffer;
return updatedMessages;
});
}
} catch (error: any) {
if (error.name === 'AbortError') {
console.log('Generation aborted');
} else {
console.error('Error fetching AI response:', error);
const errorMessage = { role: 'assistant' as const, content: 'Sorry, I encountered an error. Please try again.' };
setMessages(prev => [...prev, errorMessage]);
}
} finally {
setIsGenerating(false);
setIsLoading(false);
abortControllerRef.current = null;
}
};
export const handleStopGeneration = (abortControllerRef: React.MutableRefObject<AbortController | null>) => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};

View File

@ -1,6 +1,6 @@
"use client" "use client"
import { useCallback, useEffect, useRef, useState } from "react" import { useEffect, useRef, useState } from "react"
import { Button } from "../ui/button" import { Button } from "../ui/button"
import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react" import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react"
import { Socket } from "socket.io-client" import { Socket } from "socket.io-client"
@ -59,7 +59,7 @@ export default function GenerateInput({
}: { }: {
regenerate?: boolean regenerate?: boolean
}) => { }) => {
if (user.generations >= 1000) { if (user.generations >= 10) {
toast.error("You reached the maximum # of generations.") toast.error("You reached the maximum # of generations.")
return return
} }
@ -84,13 +84,6 @@ export default function GenerateInput({
} }
) )
} }
const handleGenerateForm = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
handleGenerate({ regenerate: false })
},
[input, currentPrompt]
)
useEffect(() => { useEffect(() => {
if (code) { if (code) {
@ -100,23 +93,9 @@ export default function GenerateInput({
} }
}, [code]) }, [code])
useEffect(() => {
//listen to when Esc key is pressed and close the modal
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
onClose()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [])
return ( return (
<div className="w-full pr-4 space-y-2"> <div className="w-full pr-4 space-y-2">
<form <div className="flex items-center font-sans space-x-2">
onSubmit={handleGenerateForm}
className="flex items-center font-sans space-x-2"
>
<input <input
ref={inputRef} ref={inputRef}
style={{ style={{
@ -130,8 +109,8 @@ export default function GenerateInput({
<Button <Button
size="sm" size="sm"
type="submit"
disabled={loading.generate || loading.regenerate || input === ""} disabled={loading.generate || loading.regenerate || input === ""}
onClick={() => handleGenerate({})}
> >
{loading.generate ? ( {loading.generate ? (
<> <>
@ -147,14 +126,13 @@ export default function GenerateInput({
</Button> </Button>
<Button <Button
onClick={onClose} onClick={onClose}
type="button"
variant="outline" variant="outline"
size="smIcon" size="smIcon"
className="bg-transparent shrink-0 border-muted-foreground" className="bg-transparent shrink-0 border-muted-foreground"
> >
<X className="h-3 w-3" /> <X className="h-3 w-3" />
</Button> </Button>
</form> </div>
{expanded ? ( {expanded ? (
<> <>
<div className="rounded-md border border-muted-foreground w-full h-28 overflow-y-scroll p-2"> <div className="rounded-md border border-muted-foreground w-full h-28 overflow-y-scroll p-2">

View File

@ -1,11 +1,10 @@
"use client" "use client"
import { SetStateAction, useCallback, useEffect, useRef, useState } from "react" import { SetStateAction, useCallback, useEffect, useRef, useState } from "react"
import * as monaco from "monaco-editor" import monaco from "monaco-editor"
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react" import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
import { toast } from "sonner" import { toast } from "sonner"
import { useClerk } from "@clerk/nextjs" import { useClerk } from "@clerk/nextjs"
import { AnimatePresence, motion } from "framer-motion"
import * as Y from "yjs" import * as Y from "yjs"
import LiveblocksProvider from "@liveblocks/yjs" import LiveblocksProvider from "@liveblocks/yjs"
@ -18,7 +17,7 @@ import {
ResizablePanel, ResizablePanel,
ResizablePanelGroup, ResizablePanelGroup,
} from "@/components/ui/resizable" } from "@/components/ui/resizable"
import { FileJson, Loader2, Sparkles, TerminalSquare, ArrowDownToLine, ArrowRightToLine } from "lucide-react" import { FileJson, Loader2, TerminalSquare } from "lucide-react"
import Tab from "../ui/tab" import Tab from "../ui/tab"
import Sidebar from "./sidebar" import Sidebar from "./sidebar"
import GenerateInput from "./generate" import GenerateInput from "./generate"
@ -31,13 +30,8 @@ import Loading from "./loading"
import PreviewWindow from "./preview" import PreviewWindow from "./preview"
import Terminals from "./terminals" import Terminals from "./terminals"
import { ImperativePanelHandle } from "react-resizable-panels" import { ImperativePanelHandle } from "react-resizable-panels"
import { PreviewProvider, usePreview } from "@/context/PreviewContext" import { PreviewProvider, usePreview } from '@/context/PreviewContext';
import { useSocket } from "@/context/SocketContext" import { useSocket } from "@/context/SocketContext"
import { Button } from "../ui/button"
import React from "react"
import { parseTSConfigToMonacoOptions } from "@/lib/tsconfig"
import { deepMerge } from "@/lib/utils"
import AIChat from "./AIChat"
export default function CodeEditor({ export default function CodeEditor({
userData, userData,
@ -46,8 +40,9 @@ export default function CodeEditor({
userData: User userData: User
sandboxData: Sandbox sandboxData: Sandbox
}) { }) {
//SocketContext functions and effects //SocketContext functions and effects
const { socket, setUserAndSandboxId } = useSocket() const { socket, setUserAndSandboxId } = useSocket();
useEffect(() => { useEffect(() => {
// Ensure userData.id and sandboxData.id are available before attempting to connect // Ensure userData.id and sandboxData.id are available before attempting to connect
@ -55,17 +50,10 @@ export default function CodeEditor({
// Check if the socket is not initialized or not connected // Check if the socket is not initialized or not connected
if (!socket || (socket && !socket.connected)) { if (!socket || (socket && !socket.connected)) {
// Initialize socket connection // Initialize socket connection
setUserAndSandboxId(userData.id, sandboxData.id) setUserAndSandboxId(userData.id, sandboxData.id);
} }
} }
}, [socket, userData.id, sandboxData.id, setUserAndSandboxId]) }, [socket, userData.id, sandboxData.id, setUserAndSandboxId]);
// This heartbeat is critical to preventing the E2B sandbox from timing out
useEffect(() => {
// 10000 ms = 10 seconds
const interval = setInterval(() => socket?.emit("heartbeat"), 10000);
return () => clearInterval(interval);
}, [socket]);
//Preview Button state //Preview Button state
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true) const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
@ -74,21 +62,12 @@ export default function CodeEditor({
message: "", message: "",
}) })
// Layout state
const [isHorizontalLayout, setIsHorizontalLayout] = useState(false);
const [previousLayout, setPreviousLayout] = useState(false);
// AI Chat state
const [isAIChatOpen, setIsAIChatOpen] = useState(false);
// File state // File state
const [files, setFiles] = useState<(TFolder | TFile)[]>([]) const [files, setFiles] = useState<(TFolder | TFile)[]>([])
const [tabs, setTabs] = useState<TTab[]>([]) const [tabs, setTabs] = useState<TTab[]>([])
const [activeFileId, setActiveFileId] = useState<string>("") const [activeFileId, setActiveFileId] = useState<string>("")
const [activeFileContent, setActiveFileContent] = useState("") const [activeFileContent, setActiveFileContent] = useState("")
const [deletingFolderId, setDeletingFolderId] = useState("") const [deletingFolderId, setDeletingFolderId] = useState("")
// Added this state to track the most recent content for each file
const [fileContents, setFileContents] = useState<Record<string, string>>({});
// Editor state // Editor state
const [editorLanguage, setEditorLanguage] = useState("plaintext") const [editorLanguage, setEditorLanguage] = useState("plaintext")
@ -97,6 +76,7 @@ export default function CodeEditor({
useState<monaco.editor.IStandaloneCodeEditor>() useState<monaco.editor.IStandaloneCodeEditor>()
// AI Copilot state // AI Copilot state
const [ai, setAi] = useState(false)
const [generate, setGenerate] = useState<{ const [generate, setGenerate] = useState<{
show: boolean show: boolean
id: string id: string
@ -109,8 +89,7 @@ export default function CodeEditor({
options: monaco.editor.IModelDeltaDecoration[] options: monaco.editor.IModelDeltaDecoration[]
instance: monaco.editor.IEditorDecorationsCollection | undefined instance: monaco.editor.IEditorDecorationsCollection | undefined
}>({ options: [], instance: undefined }) }>({ options: [], instance: undefined })
const [isSelected, setIsSelected] = useState(false)
const [showSuggestion, setShowSuggestion] = useState(false)
// Terminal state // Terminal state
const [terminals, setTerminals] = useState< const [terminals, setTerminals] = useState<
{ {
@ -120,13 +99,13 @@ export default function CodeEditor({
>([]) >([])
// Preview state // Preview state
const [previewURL, setPreviewURL] = useState<string>("") const [previewURL, setPreviewURL] = useState<string>("");
const loadPreviewURL = (url: string) => { const loadPreviewURL = (url: string) => {
// This will cause a reload if previewURL changed. // This will cause a reload if previewURL changed.
setPreviewURL(url) setPreviewURL(url);
// If the URL didn't change, still reload the preview. // If the URL didn't change, still reload the preview.
previewWindowRef.current?.refreshIframe() previewWindowRef.current?.refreshIframe();
} }
const isOwner = sandboxData.userId === userData.id const isOwner = sandboxData.userId === userData.id
@ -139,29 +118,23 @@ export default function CodeEditor({
// Liveblocks providers map to prevent reinitializing providers // Liveblocks providers map to prevent reinitializing providers
type ProviderData = { type ProviderData = {
provider: LiveblocksProvider<never, never, never, never> provider: LiveblocksProvider<never, never, never, never>;
yDoc: Y.Doc yDoc: Y.Doc;
yText: Y.Text yText: Y.Text;
binding?: MonacoBinding binding?: MonacoBinding;
onSync: (isSynced: boolean) => void onSync: (isSynced: boolean) => void;
} };
const providersMap = useRef(new Map<string, ProviderData>()) const providersMap = useRef(new Map<string, ProviderData>());
// Refs for libraries / features // Refs for libraries / features
const editorContainerRef = useRef<HTMLDivElement>(null) const editorContainerRef = useRef<HTMLDivElement>(null)
const monacoRef = useRef<typeof monaco | null>(null) const monacoRef = useRef<typeof monaco | null>(null)
const generateRef = useRef<HTMLDivElement>(null) const generateRef = useRef<HTMLDivElement>(null)
const suggestionRef = useRef<HTMLDivElement>(null)
const generateWidgetRef = useRef<HTMLDivElement>(null) const generateWidgetRef = useRef<HTMLDivElement>(null)
const { previewPanelRef } = usePreview(); const previewPanelRef = useRef<ImperativePanelHandle>(null)
const editorPanelRef = useRef<ImperativePanelHandle>(null) const editorPanelRef = useRef<ImperativePanelHandle>(null)
const previewWindowRef = useRef<{ refreshIframe: () => void }>(null) const previewWindowRef = useRef<{ refreshIframe: () => void }>(null)
const debouncedSetIsSelected = useRef(
debounce((value: boolean) => {
setIsSelected(value)
}, 800) //
).current
// Pre-mount editor keybindings // Pre-mount editor keybindings
const handleEditorWillMount: BeforeMount = (monaco) => { const handleEditorWillMount: BeforeMount = (monaco) => {
monaco.editor.addKeybindingRules([ monaco.editor.addKeybindingRules([
@ -173,87 +146,11 @@ export default function CodeEditor({
} }
// Post-mount editor keybindings and actions // Post-mount editor keybindings and actions
const handleEditorMount: OnMount = async (editor, monaco) => { const handleEditorMount: OnMount = (editor, monaco) => {
setEditorRef(editor) setEditorRef(editor)
monacoRef.current = monaco monacoRef.current = monaco
/**
* Sync all the models to the worker eagerly.
* This enables intelliSense for all files without needing an `addExtraLib` call.
*/
monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true)
monaco.languages.typescript.javascriptDefaults.setEagerModelSync(true)
monaco.languages.typescript.typescriptDefaults.setCompilerOptions(
defaultCompilerOptions
)
monaco.languages.typescript.javascriptDefaults.setCompilerOptions(
defaultCompilerOptions
)
const fetchFileContent = (fileId: string): Promise<string> => {
return new Promise((resolve) => {
socket?.emit("getFile", fileId, (content: string) => {
resolve(content)
})
})
}
const loadTSConfig = async (files: (TFolder | TFile)[]) => {
const tsconfigFiles = files.filter((file) =>
file.name.endsWith("tsconfig.json")
)
let mergedConfig: any = { compilerOptions: {} }
for (const file of tsconfigFiles) {
const containerId = file.id.split("/").slice(0, 2).join("/")
const content = await fetchFileContent(file.id)
try {
let tsConfig = JSON.parse(content)
// Handle references
if (tsConfig.references) {
for (const ref of tsConfig.references) {
const path = ref.path.replace("./", "")
const fileId = `${containerId}/${path}`
const refContent = await fetchFileContent(fileId)
const referenceTsConfig = JSON.parse(refContent)
// Merge configurations
mergedConfig = deepMerge(mergedConfig, referenceTsConfig)
}
}
// Merge current file's config
mergedConfig = deepMerge(mergedConfig, tsConfig)
} catch (error) {
console.error("Error parsing TSConfig:", error)
}
}
// Apply merged compiler options
if (mergedConfig.compilerOptions) {
const updatedOptions = parseTSConfigToMonacoOptions({
...defaultCompilerOptions,
...mergedConfig.compilerOptions,
})
monaco.languages.typescript.typescriptDefaults.setCompilerOptions(
updatedOptions
)
monaco.languages.typescript.javascriptDefaults.setCompilerOptions(
updatedOptions
)
}
}
// Call the function with your file structure
await loadTSConfig(files)
editor.onDidChangeCursorPosition((e) => { editor.onDidChangeCursorPosition((e) => {
setIsSelected(false)
const selection = editor.getSelection()
if (selection !== null) {
const hasSelection = !selection.isEmpty()
debouncedSetIsSelected(hasSelection)
setShowSuggestion(hasSelection)
}
const { column, lineNumber } = e.position const { column, lineNumber } = e.position
if (lineNumber === cursorLine) return if (lineNumber === cursorLine) return
setCursorLine(lineNumber) setCursorLine(lineNumber)
@ -307,52 +204,21 @@ export default function CodeEditor({
}, },
}) })
} }
const handleAiEdit = React.useCallback(() => {
if (!editorRef) return
const selection = editorRef.getSelection()
if (!selection) return
const pos = selection.getPosition()
const start = selection.getStartPosition()
const end = selection.getEndPosition()
let pref: monaco.editor.ContentWidgetPositionPreference
let id = ""
const isMultiline = start.lineNumber !== end.lineNumber
if (isMultiline) {
if (pos.lineNumber <= start.lineNumber) {
pref = monaco.editor.ContentWidgetPositionPreference.ABOVE
} else {
pref = monaco.editor.ContentWidgetPositionPreference.BELOW
}
} else {
pref = monaco.editor.ContentWidgetPositionPreference.ABOVE
}
editorRef.changeViewZones(function (changeAccessor) {
if (!generateRef.current) return
if (pref === monaco.editor.ContentWidgetPositionPreference.ABOVE) {
id = changeAccessor.addZone({
afterLineNumber: start.lineNumber - 1,
heightInLines: 2,
domNode: generateRef.current,
})
}
})
setGenerate((prev) => {
return {
...prev,
show: true,
pref: [pref],
id,
}
})
}, [editorRef])
// Generate widget effect // Generate widget effect
useEffect(() => { useEffect(() => {
if (!ai) {
setGenerate((prev) => {
return {
...prev,
show: false,
}
})
return
}
if (generate.show) { if (generate.show) {
setShowSuggestion(false)
editorRef?.changeViewZones(function (changeAccessor) { editorRef?.changeViewZones(function (changeAccessor) {
if (!generateRef.current) return if (!generateRef.current) return
if (!generate.id) {
const id = changeAccessor.addZone({ const id = changeAccessor.addZone({
afterLineNumber: cursorLine, afterLineNumber: cursorLine,
heightInLines: 3, heightInLines: 3,
@ -361,10 +227,6 @@ export default function CodeEditor({
setGenerate((prev) => { setGenerate((prev) => {
return { ...prev, id, line: cursorLine } return { ...prev, id, line: cursorLine }
}) })
}
setGenerate((prev) => {
return { ...prev, line: cursorLine }
})
}) })
if (!generateWidgetRef.current) return if (!generateWidgetRef.current) return
@ -425,64 +287,21 @@ export default function CodeEditor({
} }
}, [generate.show]) }, [generate.show])
// Suggestion widget effect
useEffect(() => {
if (!suggestionRef.current || !editorRef) return
const widgetElement = suggestionRef.current
const suggestionWidget: monaco.editor.IContentWidget = {
getDomNode: () => {
return widgetElement
},
getId: () => {
return "suggestion.widget"
},
getPosition: () => {
const selection = editorRef?.getSelection()
const column = Math.max(3, selection?.positionColumn ?? 1)
let lineNumber = selection?.positionLineNumber ?? 1
let pref = monaco.editor.ContentWidgetPositionPreference.ABOVE
if (lineNumber <= 3) {
pref = monaco.editor.ContentWidgetPositionPreference.BELOW
}
return {
preference: [pref],
position: {
lineNumber,
column,
},
}
},
}
if (isSelected) {
editorRef?.addContentWidget(suggestionWidget)
editorRef?.applyFontInfo(suggestionRef.current)
} else {
editorRef?.removeContentWidget(suggestionWidget)
}
}, [isSelected])
// Decorations effect for generate widget tips // Decorations effect for generate widget tips
useEffect(() => { useEffect(() => {
if (decorations.options.length === 0) { if (decorations.options.length === 0) {
decorations.instance?.clear() decorations.instance?.clear()
} }
if (!ai) return
const model = editorRef?.getModel() const model = editorRef?.getModel()
// added this because it was giving client side exception - Illegal value for lineNumber when opening an empty file const line = model?.getLineContent(cursorLine)
if (model) {
const totalLines = model.getLineCount(); if (line === undefined || line.trim() !== "") {
// Check if the cursorLine is a valid number, If cursorLine is out of bounds, we fall back to 1 (the first line) as a default safe value. decorations.instance?.clear()
const lineNumber = cursorLine > 0 && cursorLine <= totalLines ? cursorLine : 1; // fallback to a valid line number
// If for some reason the content doesn't exist, we use an empty string as a fallback.
const line = model.getLineContent(lineNumber) ?? "";
// Check if the line is not empty or only whitespace (i.e., `.trim()` removes spaces).
// If the line has content, we clear any decorations using the instance of the `decorations` object.
// Decorations refer to editor highlights, underlines, or markers, so this clears those if conditions are met.
if (line.trim() !== "") {
decorations.instance?.clear();
return return
} }
}
if (decorations.instance) { if (decorations.instance) {
decorations.instance.set(decorations.options) decorations.instance.set(decorations.options)
@ -500,50 +319,33 @@ export default function CodeEditor({
}, [decorations.options]) }, [decorations.options])
// Save file keybinding logic effect // Save file keybinding logic effect
// Function to save the file content after a debounce period
const debouncedSaveData = useCallback( const debouncedSaveData = useCallback(
debounce((activeFileId: string | undefined) => { debounce((value: string | undefined, activeFileId: string | undefined) => {
if (activeFileId) {
// Get the current content of the file
const content = fileContents[activeFileId];
// Mark the file as saved in the tabs
setTabs((prev) => setTabs((prev) =>
prev.map((tab) => prev.map((tab) =>
tab.id === activeFileId ? { ...tab, saved: true } : tab tab.id === activeFileId ? { ...tab, saved: true } : tab
) )
); );
console.log(`Saving file...${activeFileId}`); console.log(`Saving file...${activeFileId}`);
console.log(`Saving file...${content}`); console.log(`Saving file...${value}`);
socket?.emit("saveFile", activeFileId, content); socket?.emit("saveFile", activeFileId, value);
}
}, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000), }, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000),
[socket, fileContents] [socket]
); );
// Keydown event listener to trigger file save on Ctrl+S or Cmd+S, and toggle AI chat on Ctrl+L or Cmd+L
useEffect(() => { useEffect(() => {
const down = (e: KeyboardEvent) => { const down = (e: KeyboardEvent) => {
if (e.key === "s" && (e.metaKey || e.ctrlKey)) { if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
e.preventDefault() e.preventDefault();
debouncedSaveData(activeFileId); debouncedSaveData(editorRef?.getValue(), activeFileId);
} else if (e.key === "l" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setIsAIChatOpen(prev => !prev);
} }
}; };
document.addEventListener("keydown", down); document.addEventListener("keydown", down);
// Added this line to prevent Monaco editor from handling Cmd/Ctrl+L
editorRef?.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyL, () => {
setIsAIChatOpen(prev => !prev);
});
return () => { return () => {
document.removeEventListener("keydown", down) document.removeEventListener("keydown", down);
} };
}, [activeFileId, tabs, debouncedSaveData, setIsAIChatOpen, editorRef]) }, [activeFileId, tabs, debouncedSaveData]);
// Liveblocks live collaboration setup effect // Liveblocks live collaboration setup effect
useEffect(() => { useEffect(() => {
@ -552,13 +354,13 @@ export default function CodeEditor({
if (!editorRef || !tab || !model) return if (!editorRef || !tab || !model) return
let providerData: ProviderData let providerData: ProviderData;
// When a file is opened for the first time, create a new provider and store in providersMap. // When a file is opened for the first time, create a new provider and store in providersMap.
if (!providersMap.current.has(tab.id)) { if (!providersMap.current.has(tab.id)) {
const yDoc = new Y.Doc() const yDoc = new Y.Doc();
const yText = yDoc.getText(tab.id) const yText = yDoc.getText(tab.id);
const yProvider = new LiveblocksProvider(room, yDoc) const yProvider = new LiveblocksProvider(room, yDoc);
// Inserts the file content into the editor once when the tab is changed. // Inserts the file content into the editor once when the tab is changed.
const onSync = (isSynced: boolean) => { const onSync = (isSynced: boolean) => {
@ -579,11 +381,12 @@ export default function CodeEditor({
yProvider.on("sync", onSync) yProvider.on("sync", onSync)
// Save the provider to the map. // Save the provider to the map.
providerData = { provider: yProvider, yDoc, yText, onSync } providerData = { provider: yProvider, yDoc, yText, onSync };
providersMap.current.set(tab.id, providerData) providersMap.current.set(tab.id, providerData);
} else { } else {
// When a tab is opened that has been open before, reuse the existing provider. // When a tab is opened that has been open before, reuse the existing provider.
providerData = providersMap.current.get(tab.id)! providerData = providersMap.current.get(tab.id)!;
} }
const binding = new MonacoBinding( const binding = new MonacoBinding(
@ -591,21 +394,21 @@ export default function CodeEditor({
model, model,
new Set([editorRef]), new Set([editorRef]),
providerData.provider.awareness as unknown as Awareness providerData.provider.awareness as unknown as Awareness
) );
providerData.binding = binding providerData.binding = binding;
setProvider(providerData.provider) setProvider(providerData.provider);
return () => { return () => {
// Cleanup logic // Cleanup logic
if (binding) { if (binding) {
binding.destroy() binding.destroy();
} }
if (providerData.binding) { if (providerData.binding) {
providerData.binding = undefined providerData.binding = undefined;
} }
} };
}, [room, activeFileContent]) }, [room, activeFileContent]);
// Added this effect to clean up when the component unmounts // Added this effect to clean up when the component unmounts
useEffect(() => { useEffect(() => {
@ -613,14 +416,14 @@ export default function CodeEditor({
// Clean up all providers when the component unmounts // Clean up all providers when the component unmounts
providersMap.current.forEach((data) => { providersMap.current.forEach((data) => {
if (data.binding) { if (data.binding) {
data.binding.destroy() data.binding.destroy();
} }
data.provider.disconnect() data.provider.disconnect();
data.yDoc.destroy() data.yDoc.destroy();
}) });
providersMap.current.clear() providersMap.current.clear();
} };
}, []) }, []);
// Connection/disconnection effect // Connection/disconnection effect
useEffect(() => { useEffect(() => {
@ -678,70 +481,49 @@ export default function CodeEditor({
socket?.off("disableAccess", onDisableAccess) socket?.off("disableAccess", onDisableAccess)
socket?.off("previewURL", loadPreviewURL) socket?.off("previewURL", loadPreviewURL)
} }
}, [ }, [socket, terminals, setTerminals, setFiles, toast, setDisableAccess, isOwner, loadPreviewURL]);
socket,
terminals,
setTerminals,
setFiles,
toast,
setDisableAccess,
isOwner,
loadPreviewURL,
])
// Helper functions for tabs: // Helper functions for tabs:
// Select file and load content // Select file and load content
// Initialize debounced function once // Initialize debounced function once
const fileCache = useRef(new Map()) const fileCache = useRef(new Map());
// Debounced function to get file content // Debounced function to get file content
const debouncedGetFile = (tabId: any, callback: any) => { const debouncedGetFile = useCallback(
socket?.emit("getFile", tabId, callback) debounce((tabId, callback) => {
} // 300ms debounce delay, adjust as needed socket?.emit('getFile', tabId, callback);
}, 300), // 300ms debounce delay, adjust as needed
[]
);
const selectFile = (tab: TTab) => { const selectFile = useCallback((tab: TTab) => {
if (tab.id === activeFileId) return; if (tab.id === activeFileId) return;
setGenerate((prev) => ({ ...prev, show: false })); setGenerate((prev) => ({ ...prev, show: false }));
// Check if the tab already exists in the list of open tabs
const exists = tabs.find((t) => t.id === tab.id); const exists = tabs.find((t) => t.id === tab.id);
setTabs((prev) => { setTabs((prev) => {
if (exists) { if (exists) {
// If the tab exists, make it the active tab
setActiveFileId(exists.id); setActiveFileId(exists.id);
return prev; return prev;
} }
// If the tab doesn't exist, add it to the list of tabs and make it active
return [...prev, tab]; return [...prev, tab];
}); });
// If the file's content is already cached, set it as the active content if (fileCache.current.has(tab.id)) {
if (fileContents[tab.id]) { setActiveFileContent(fileCache.current.get(tab.id));
setActiveFileContent(fileContents[tab.id]);
} else { } else {
// Otherwise, fetch the content of the file and cache it debouncedGetFile(tab.id, (response: SetStateAction<string>) => {
debouncedGetFile(tab.id, (response: string) => { fileCache.current.set(tab.id, response);
setFileContents(prev => ({ ...prev, [tab.id]: response }));
setActiveFileContent(response); setActiveFileContent(response);
}); });
} }
// Set the editor language based on the file type
setEditorLanguage(processFileType(tab.name)); setEditorLanguage(processFileType(tab.name));
// Set the active file ID to the new tab
setActiveFileId(tab.id); setActiveFileId(tab.id);
}; }, [activeFileId, tabs, debouncedGetFile]);
// Added this effect to update fileContents when the editor content changes
useEffect(() => {
if (activeFileId) {
// Cache the current active file content using the file ID as the key
setFileContents(prev => ({ ...prev, [activeFileId]: activeFileContent }));
}
}, [activeFileContent, activeFileId]);
// Close tab and remove from tabs // Close tab and remove from tabs
const closeTab = (id: string) => { const closeTab = (id: string) => {
@ -844,37 +626,6 @@ export default function CodeEditor({
}) })
} }
const togglePreviewPanel = () => {
if (isPreviewCollapsed) {
previewPanelRef.current?.expand();
setIsPreviewCollapsed(false);
} else {
previewPanelRef.current?.collapse();
setIsPreviewCollapsed(true);
}
};
const toggleLayout = () => {
if (!isAIChatOpen) {
setIsHorizontalLayout(prev => !prev);
}
};
// Add an effect to handle layout changes when AI chat is opened/closed
useEffect(() => {
if (isAIChatOpen) {
setPreviousLayout(isHorizontalLayout);
setIsHorizontalLayout(true);
} else {
setIsHorizontalLayout(previousLayout);
}
}, [isAIChatOpen]);
// Modify the toggleAIChat function
const toggleAIChat = () => {
setIsAIChatOpen(prev => !prev);
};
// On disabled access for shared users, show un-interactable loading placeholder + info modal // On disabled access for shared users, show un-interactable loading placeholder + info modal
if (disableAccess.isDisabled) if (disableAccess.isDisabled)
return ( return (
@ -893,74 +644,30 @@ export default function CodeEditor({
{/* Copilot DOM elements */} {/* Copilot DOM elements */}
<PreviewProvider> <PreviewProvider>
<div ref={generateRef} /> <div ref={generateRef} />
<div ref={suggestionRef} className="absolute">
<AnimatePresence>
{isSelected && showSuggestion && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ ease: "easeOut", duration: 0.2 }}
>
<Button size="xs" type="submit" onClick={handleAiEdit}>
<Sparkles className="h-3 w-3 mr-1" />
Edit Code
</Button>
</motion.div>
)}
</AnimatePresence>
</div>
<div className="z-50 p-1" ref={generateWidgetRef}> <div className="z-50 p-1" ref={generateWidgetRef}>
{generate.show ? ( {generate.show && ai ? (
<GenerateInput <GenerateInput
user={userData} user={userData}
socket={socket!} socket={socket!}
width={generate.width - 90} width={generate.width - 90}
data={{ data={{
fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "", fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "",
code: code: editorRef?.getValue() ?? "",
(isSelected && editorRef?.getSelection()
? editorRef
?.getModel()
?.getValueInRange(editorRef?.getSelection()!)
: editorRef?.getValue()) ?? "",
line: generate.line, line: generate.line,
}} }}
editor={{ editor={{
language: editorLanguage, language: editorLanguage,
}} }}
onExpand={() => { onExpand={() => {
const line = generate.line
editorRef?.changeViewZones(function (changeAccessor) { editorRef?.changeViewZones(function (changeAccessor) {
changeAccessor.removeZone(generate.id) changeAccessor.removeZone(generate.id)
if (!generateRef.current) return if (!generateRef.current) return
let id = "" const id = changeAccessor.addZone({
if (isSelected) {
const selection = editorRef?.getSelection()
if (!selection) return
const isAbove =
generate.pref?.[0] ===
monaco.editor.ContentWidgetPositionPreference.ABOVE
const afterLineNumber = isAbove ? line - 1 : line
id = changeAccessor.addZone({
afterLineNumber,
heightInLines: isAbove ? 11 : 12,
domNode: generateRef.current,
})
const contentWidget = generate.widget
if (contentWidget) {
editorRef?.layoutContentWidget(contentWidget)
}
} else {
id = changeAccessor.addZone({
afterLineNumber: cursorLine, afterLineNumber: cursorLine,
heightInLines: 12, heightInLines: 12,
domNode: generateRef.current, domNode: generateRef.current,
}) })
}
setGenerate((prev) => { setGenerate((prev) => {
return { ...prev, id } return { ...prev, id }
}) })
@ -974,14 +681,12 @@ export default function CodeEditor({
show: !prev.show, show: !prev.show,
} }
}) })
const selection = editorRef?.getSelection() const file = editorRef?.getValue()
const range =
isSelected && selection const lines = file?.split("\n") || []
? selection lines.splice(line - 1, 0, code)
: new monaco.Range(line, 1, line, 1) const updatedFile = lines.join("\n")
editorRef?.executeEdits("ai-generation", [ editorRef?.setValue(updatedFile)
{ range, text: code, forceMoveMarkers: true },
])
}} }}
onClose={() => { onClose={() => {
setGenerate((prev) => { setGenerate((prev) => {
@ -994,6 +699,7 @@ export default function CodeEditor({
/> />
) : null} ) : null}
</div> </div>
{/* Main editor components */} {/* Main editor components */}
<Sidebar <Sidebar
sandboxData={sandboxData} sandboxData={sandboxData}
@ -1006,17 +712,18 @@ export default function CodeEditor({
setFiles={setFiles} setFiles={setFiles}
addNew={(name, type) => addNew(name, type, setFiles, sandboxData)} addNew={(name, type) => addNew(name, type, setFiles, sandboxData)}
deletingFolderId={deletingFolderId} deletingFolderId={deletingFolderId}
// AI Copilot Toggle
ai={ai}
setAi={setAi}
/> />
{/* Outer ResizablePanelGroup for main layout */}
<ResizablePanelGroup direction={isHorizontalLayout ? "horizontal" : "vertical"}> {/* Shadcn resizeable panels: https://ui.shadcn.com/docs/components/resizable */}
{/* Left side: Editor and Preview/Terminal */} <ResizablePanelGroup direction="horizontal">
<ResizablePanel defaultSize={isAIChatOpen ? 80 : 100} minSize={50}>
<ResizablePanelGroup direction={isHorizontalLayout ? "vertical" : "horizontal"}>
<ResizablePanel <ResizablePanel
className="p-2 flex flex-col" className="p-2 flex flex-col"
maxSize={80} maxSize={80}
minSize={30} minSize={30}
defaultSize={70} defaultSize={60}
ref={editorPanelRef} ref={editorPanelRef}
> >
<div className="h-10 w-full flex gap-2 overflow-auto tab-scroll"> <div className="h-10 w-full flex gap-2 overflow-auto tab-scroll">
@ -1059,19 +766,7 @@ export default function CodeEditor({
beforeMount={handleEditorWillMount} beforeMount={handleEditorWillMount}
onMount={handleEditorMount} onMount={handleEditorMount}
onChange={(value) => { onChange={(value) => {
// If the new content is different from the cached content, update it if (value === activeFileContent) {
if (value !== fileContents[activeFileId]) {
setActiveFileContent(value ?? ""); // Update the active file content
// Mark the file as unsaved by setting 'saved' to false
setTabs((prev) =>
prev.map((tab) =>
tab.id === activeFileId
? { ...tab, saved: false }
: tab
)
)
} else {
// If the content matches the cached content, mark the file as saved
setTabs((prev) => setTabs((prev) =>
prev.map((tab) => prev.map((tab) =>
tab.id === activeFileId tab.id === activeFileId
@ -1079,6 +774,14 @@ export default function CodeEditor({
: tab : tab
) )
) )
} else {
setTabs((prev) =>
prev.map((tab) =>
tab.id === activeFileId
? { ...tab, saved: false }
: tab
)
)
} }
}} }}
options={{ options={{
@ -1107,49 +810,23 @@ export default function CodeEditor({
</div> </div>
</ResizablePanel> </ResizablePanel>
<ResizableHandle /> <ResizableHandle />
<ResizablePanel defaultSize={30}> <ResizablePanel defaultSize={40}>
<ResizablePanelGroup direction={ <ResizablePanelGroup direction="vertical">
isAIChatOpen && isHorizontalLayout ? "horizontal" :
isAIChatOpen ? "vertical" :
isHorizontalLayout ? "horizontal" :
"vertical"
}>
<ResizablePanel <ResizablePanel
ref={previewPanelRef} ref={usePreview().previewPanelRef}
defaultSize={isPreviewCollapsed ? 4 : 20} defaultSize={4}
collapsedSize={4}
minSize={25} minSize={25}
collapsedSize={isHorizontalLayout ? 20 : 4}
className="p-2 flex flex-col"
collapsible collapsible
className="p-2 flex flex-col"
onCollapse={() => setIsPreviewCollapsed(true)} onCollapse={() => setIsPreviewCollapsed(true)}
onExpand={() => setIsPreviewCollapsed(false)} onExpand={() => setIsPreviewCollapsed(false)}
> >
<div className="flex items-center justify-between">
<Button
onClick={toggleLayout}
size="sm"
variant="ghost"
className="mr-2 border"
disabled={isAIChatOpen}
>
{isHorizontalLayout ? <ArrowRightToLine className="w-4 h-4" /> : <ArrowDownToLine className="w-4 h-4" />}
</Button>
<PreviewWindow <PreviewWindow
open={togglePreviewPanel} open={() => {
collapsed={isPreviewCollapsed} usePreview().previewPanelRef.current?.expand()
src={previewURL} setIsPreviewCollapsed(false)
ref={previewWindowRef} }} collapsed={isPreviewCollapsed} src={previewURL} ref={previewWindowRef} />
/>
</div>
{!isPreviewCollapsed && (
<div className="w-full grow rounded-md overflow-hidden bg-foreground mt-2">
<iframe
width={"100%"}
height={"100%"}
src={previewURL}
/>
</div>
)}
</ResizablePanel> </ResizablePanel>
<ResizableHandle /> <ResizableHandle />
<ResizablePanel <ResizablePanel
@ -1169,37 +846,8 @@ export default function CodeEditor({
</ResizablePanelGroup> </ResizablePanelGroup>
</ResizablePanel> </ResizablePanel>
</ResizablePanelGroup> </ResizablePanelGroup>
</ResizablePanel>
{/* Right side: AIChat (if open) */}
{isAIChatOpen && (
<>
<ResizableHandle />
<ResizablePanel defaultSize={30} minSize={15}>
<AIChat
activeFileContent={activeFileContent}
activeFileName={tabs.find(tab => tab.id === activeFileId)?.name || 'No file selected'}
onClose={toggleAIChat}
/>
</ResizablePanel>
</>
)}
</ResizablePanelGroup>
</PreviewProvider> </PreviewProvider>
</> </>
) )
} }
/**
* Configure the typescript compiler to detect JSX and load type definitions
*/
const defaultCompilerOptions: monaco.languages.typescript.CompilerOptions = {
allowJs: true,
allowSyntheticDefaultImports: true,
allowNonTsExtensions: true,
resolveJsonModule: true,
jsx: monaco.languages.typescript.JsxEmit.ReactJSX,
module: monaco.languages.typescript.ModuleKind.ESNext,
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
target: monaco.languages.typescript.ScriptTarget.ESNext,
}

View File

@ -1,6 +1,5 @@
"use client"; "use client";
import React, { useEffect, useRef } from 'react';
import { Play, StopCircle } from "lucide-react"; import { Play, StopCircle } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useTerminal } from "@/context/TerminalContext"; import { useTerminal } from "@/context/TerminalContext";
@ -17,57 +16,53 @@ export default function RunButtonModal({
setIsRunning: (running: boolean) => void; setIsRunning: (running: boolean) => void;
sandboxData: Sandbox; sandboxData: Sandbox;
}) { }) {
const { createNewTerminal, closeTerminal, terminals } = useTerminal(); const { createNewTerminal, terminals, closeTerminal } = useTerminal();
const { setIsPreviewCollapsed, previewPanelRef } = usePreview(); const { setIsPreviewCollapsed, previewPanelRef } = usePreview();
// Ref to keep track of the last created terminal's ID
const lastCreatedTerminalRef = useRef<string | null>(null);
// Effect to update the lastCreatedTerminalRef when a new terminal is added const handleRun = () => {
useEffect(() => { if (isRunning) {
if (terminals.length > 0 && !isRunning) { console.log('Stopping sandbox...');
const latestTerminal = terminals[terminals.length - 1]; console.log('Closing Preview Window');
if (latestTerminal && latestTerminal.id !== lastCreatedTerminalRef.current) {
lastCreatedTerminalRef.current = latestTerminal.id; terminals.forEach(term => {
} if (term.terminal) {
} closeTerminal(term.id);
}, [terminals, isRunning]); console.log('Closing Terminal', term.id);
}
});
const handleRun = async () => {
if (isRunning && lastCreatedTerminalRef.current)
{
await closeTerminal(lastCreatedTerminalRef.current);
lastCreatedTerminalRef.current = null;
setIsPreviewCollapsed(true); setIsPreviewCollapsed(true);
previewPanelRef.current?.collapse(); previewPanelRef.current?.collapse();
} } else {
else if (!isRunning && terminals.length < 4) console.log('Running sandbox...');
{ console.log('Opening Terminal');
const command = sandboxData.type === "streamlit" console.log('Opening Preview Window');
? "pip install -r requirements.txt && streamlit run main.py --server.runOnSave true"
: "yarn install && yarn dev"; if (terminals.length < 4) {
if (sandboxData.type === "streamlit") {
createNewTerminal(
"pip install -r requirements.txt && streamlit run main.py --server.runOnSave true"
);
} else {
createNewTerminal("yarn install && yarn dev");
}
} else {
toast.error("You reached the maximum # of terminals.");
console.error("Maximum number of terminals reached.");
}
try {
// Create a new terminal with the appropriate command
await createNewTerminal(command);
setIsPreviewCollapsed(false); setIsPreviewCollapsed(false);
previewPanelRef.current?.expand(); previewPanelRef.current?.expand();
} catch (error) {
toast.error("Failed to create new terminal.");
console.error("Error creating new terminal:", error);
return;
} }
} else if (!isRunning) {
toast.error("You've reached the maximum number of terminals.");
return;
}
setIsRunning(!isRunning); setIsRunning(!isRunning);
}; };
return ( return (
<>
<Button variant="outline" onClick={handleRun}> <Button variant="outline" onClick={handleRun}>
{isRunning ? <StopCircle className="w-4 h-4 mr-2" /> : <Play className="w-4 h-4 mr-2" />} {isRunning ? <StopCircle className="w-4 h-4 mr-2" /> : <Play className="w-4 h-4 mr-2" />}
{isRunning ? 'Stop' : 'Run'} {isRunning ? 'Stop' : 'Run'}
</Button> </Button>
</>
); );
} }

View File

@ -4,7 +4,6 @@ import {
Link, Link,
RotateCw, RotateCw,
TerminalSquare, TerminalSquare,
UnfoldVertical,
} from "lucide-react" } from "lucide-react"
import { useEffect, useRef, useState, useImperativeHandle, forwardRef } from "react" import { useEffect, useRef, useState, useImperativeHandle, forwardRef } from "react"
import { toast } from "sonner" import { toast } from "sonner"
@ -33,18 +32,24 @@ ref: React.Ref<{
return ( return (
<> <>
<div
className={`${collapsed ? "h-full" : "h-10"
} select-none w-full flex gap-2`}
>
<div className="h-8 rounded-md px-3 bg-secondary flex items-center w-full justify-between"> <div className="h-8 rounded-md px-3 bg-secondary flex items-center w-full justify-between">
<div className="text-xs">Preview</div> <div className="text-xs">Preview</div>
<div className="flex space-x-1 translate-x-1"> <div className="flex space-x-1 translate-x-1">
{collapsed ? ( {collapsed ? (
<PreviewButton onClick={open}> <PreviewButton disabled onClick={() => { }}>
<UnfoldVertical className="w-4 h-4" /> <TerminalSquare className="w-4 h-4" />
</PreviewButton> </PreviewButton>
) : ( ) : (
<> <>
{/* Removed the unfoldvertical button since we have the same thing via the run button.
<PreviewButton onClick={open}> <PreviewButton onClick={open}>
<UnfoldVertical className="w-4 h-4" /> <UnfoldVertical className="w-4 h-4" />
</PreviewButton> </PreviewButton> */}
<PreviewButton <PreviewButton
onClick={() => { onClick={() => {
@ -61,6 +66,18 @@ ref: React.Ref<{
)} )}
</div> </div>
</div> </div>
</div>
{collapsed ? null : (
<div className="w-full grow rounded-md overflow-hidden bg-foreground">
<iframe
key={iframeKey}
ref={frameRef}
width={"100%"}
height={"100%"}
src={src}
/>
</div>
)}
</> </>
) )
}) })

View File

@ -90,9 +90,9 @@ export default function SidebarFile({
if (!editing && !pendingDelete && !isMoving) if (!editing && !pendingDelete && !isMoving)
selectFile({ ...data, saved: true }); selectFile({ ...data, saved: true });
}} }}
onDoubleClick={() => { // onDoubleClick={() => {
setEditing(true) // setEditing(true)
}} // }}
className={`${ className={`${
dragging ? "opacity-50 hover:!bg-background" : "" dragging ? "opacity-50 hover:!bg-background" : ""
} data-[state=open]:bg-secondary/50 w-full flex items-center h-7 px-1 hover:bg-secondary rounded-sm cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring`} } data-[state=open]:bg-secondary/50 w-full flex items-center h-7 px-1 hover:bg-secondary rounded-sm cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring`}

View File

@ -1,20 +1,18 @@
"use client" "use client";
import Image from "next/image" import Image from "next/image";
import { useEffect, useRef, useState } from "react" import { useEffect, useRef, useState } from "react";
import { getIconForFolder, getIconForOpenFolder } from "vscode-icons-js" import { getIconForFolder, getIconForOpenFolder } from "vscode-icons-js";
import { TFile, TFolder, TTab } from "@/lib/types" import { TFile, TFolder, TTab } from "@/lib/types";
import SidebarFile from "./file" import SidebarFile from "./file";
import { import {
ContextMenu, ContextMenu,
ContextMenuContent, ContextMenuContent,
ContextMenuItem, ContextMenuItem,
ContextMenuTrigger, ContextMenuTrigger,
} from "@/components/ui/context-menu" } from "@/components/ui/context-menu";
import { ChevronRight, Loader2, Pencil, Trash2 } from "lucide-react" import { Loader2, Pencil, Trash2 } from "lucide-react";
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter" import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { cn } from "@/lib/utils"
import { motion, AnimatePresence } from "framer-motion"
// Note: Renaming has not been implemented in the backend yet, so UI relating to renaming is commented out // Note: Renaming has not been implemented in the backend yet, so UI relating to renaming is commented out
@ -27,27 +25,27 @@ export default function SidebarFolder({
movingId, movingId,
deletingFolderId, deletingFolderId,
}: { }: {
data: TFolder data: TFolder;
selectFile: (file: TTab) => void selectFile: (file: TTab) => void;
handleRename: ( handleRename: (
id: string, id: string,
newName: string, newName: string,
oldName: string, oldName: string,
type: "file" | "folder" type: "file" | "folder"
) => boolean ) => boolean;
handleDeleteFile: (file: TFile) => void handleDeleteFile: (file: TFile) => void;
handleDeleteFolder: (folder: TFolder) => void handleDeleteFolder: (folder: TFolder) => void;
movingId: string movingId: string;
deletingFolderId: string deletingFolderId: string;
}) { }) {
const ref = useRef(null) // drop target const ref = useRef(null); // drop target
const [isDraggedOver, setIsDraggedOver] = useState(false) const [isDraggedOver, setIsDraggedOver] = useState(false);
const isDeleting = const isDeleting =
deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId) deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId);
useEffect(() => { useEffect(() => {
const el = ref.current const el = ref.current;
if (el) if (el)
return dropTargetForElements({ return dropTargetForElements({
@ -69,17 +67,17 @@ export default function SidebarFolder({
// no dropping while awaiting move // no dropping while awaiting move
canDrop: () => { canDrop: () => {
return !movingId return !movingId;
}, },
}) });
}, []) }, []);
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false);
const folder = isOpen const folder = isOpen
? getIconForOpenFolder(data.name) ? getIconForOpenFolder(data.name)
: getIconForFolder(data.name) : getIconForFolder(data.name);
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null);
// const [editing, setEditing] = useState(false); // const [editing, setEditing] = useState(false);
// useEffect(() => { // useEffect(() => {
@ -98,12 +96,6 @@ export default function SidebarFolder({
isDraggedOver ? "bg-secondary/50 rounded-t-sm" : "rounded-sm" isDraggedOver ? "bg-secondary/50 rounded-t-sm" : "rounded-sm"
} w-full flex items-center h-7 px-1 transition-colors hover:bg-secondary cursor-pointer`} } w-full flex items-center h-7 px-1 transition-colors hover:bg-secondary cursor-pointer`}
> >
<ChevronRight
className={cn(
"min-w-3 min-h-3 mr-1 ml-auto transition-all duration-300",
isOpen ? "transform rotate-90" : ""
)}
/>
<Image <Image
src={`/icons/${folder}`} src={`/icons/${folder}`}
alt="Folder icon" alt="Folder icon"
@ -157,36 +149,21 @@ export default function SidebarFolder({
<ContextMenuItem <ContextMenuItem
disabled={isDeleting} disabled={isDeleting}
onClick={() => { onClick={() => {
handleDeleteFolder(data) handleDeleteFolder(data);
}} }}
> >
<Trash2 className="w-4 h-4 mr-2" /> <Trash2 className="w-4 h-4 mr-2" />
Delete Delete
</ContextMenuItem> </ContextMenuItem>
</ContextMenuContent> </ContextMenuContent>
<AnimatePresence>
{isOpen ? ( {isOpen ? (
<motion.div
className="overflow-y-hidden"
initial={{
height: 0,
opacity: 0,
}}
animate={{
height: "auto",
opacity: 1,
}}
exit={{
height: 0,
opacity: 0,
}}
>
<div <div
className={cn( className={`flex w-full items-stretch ${
isDraggedOver ? "rounded-b-sm bg-secondary/50" : "" isDraggedOver ? "rounded-b-sm bg-secondary/50" : ""
)} }`}
> >
<div className="flex flex-col grow ml-2 pl-2 border-l border-border"> <div className="w-[1px] bg-border mx-2 h-full"></div>
<div className="flex flex-col grow">
{data.children.map((child) => {data.children.map((child) =>
child.type === "file" ? ( child.type === "file" ? (
<SidebarFile <SidebarFile
@ -213,9 +190,7 @@ export default function SidebarFolder({
)} )}
</div> </div>
</div> </div>
</motion.div>
) : null} ) : null}
</AnimatePresence>
</ContextMenu> </ContextMenu>
) );
} }

View File

@ -4,8 +4,9 @@ import {
FilePlus, FilePlus,
FolderPlus, FolderPlus,
Loader2, Loader2,
MonitorPlay,
Search,
Sparkles, Sparkles,
MessageSquareMore,
} from "lucide-react"; } from "lucide-react";
import SidebarFile from "./file"; import SidebarFile from "./file";
import SidebarFolder from "./folder"; import SidebarFolder from "./folder";
@ -13,12 +14,13 @@ import { Sandbox, TFile, TFolder, TTab } from "@/lib/types";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import New from "./new"; import New from "./new";
import { Socket } from "socket.io-client"; import { Socket } from "socket.io-client";
import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch";
import { import {
dropTargetForElements, dropTargetForElements,
monitorForElements, monitorForElements,
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import Button from "@/components/ui/customButton";
export default function Sidebar({ export default function Sidebar({
sandboxData, sandboxData,
@ -30,6 +32,8 @@ export default function Sidebar({
socket, socket,
setFiles, setFiles,
addNew, addNew,
ai,
setAi,
deletingFolderId, deletingFolderId,
}: { }: {
sandboxData: Sandbox; sandboxData: Sandbox;
@ -46,6 +50,8 @@ export default function Sidebar({
socket: Socket; socket: Socket;
setFiles: (files: (TFile | TFolder)[]) => void; setFiles: (files: (TFile | TFolder)[]) => void;
addNew: (name: string, type: "file" | "folder") => void; addNew: (name: string, type: "file" | "folder") => void;
ai: boolean;
setAi: React.Dispatch<React.SetStateAction<boolean>>;
deletingFolderId: string; deletingFolderId: string;
}) { }) {
const ref = useRef(null); // drop target const ref = useRef(null); // drop target
@ -103,8 +109,8 @@ export default function Sidebar({
}, []); }, []);
return ( return (
<div className="h-full w-56 select-none flex flex-col text-sm"> <div className="h-full w-56 select-none flex flex-col text-sm items-start justify-between p-2">
<div className="flex-grow overflow-auto p-2 pb-[84px]"> <div className="w-full flex flex-col items-start">
<div className="flex w-full items-center justify-between h-8 mb-1 "> <div className="flex w-full items-center justify-between h-8 mb-1 ">
<div className="text-muted-foreground">Explorer</div> <div className="text-muted-foreground">Explorer</div>
<div className="flex space-x-1"> <div className="flex space-x-1">
@ -179,25 +185,24 @@ export default function Sidebar({
)} )}
</div> </div>
</div> </div>
<div className="fixed bottom-0 w-48 flex flex-col p-2 bg-background"> <div className="w-full space-y-4">
<Button variant="ghost" className="w-full justify-start text-sm text-muted-foreground font-normal h-8 px-2 mb-2" disabled aria-disabled="true" style={{ opacity: 1}}> <div className="flex items-center justify-between w-full">
<Sparkles className="h-4 w-4 mr-2 text-indigo-500 opacity-70" /> <div className="flex items-center">
Copilot <Sparkles
<div className="ml-auto"> className={`h-4 w-4 mr-2 ${
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground"> ai ? "text-indigo-500" : "text-muted-foreground"
<span className="text-xs"></span>G }`}
</kbd> />
Copilot{" "}
<span className="font-mono text-muted-foreground inline-block ml-1.5 text-xs leading-none border border-b-2 border-muted-foreground py-1 px-1.5 rounded-md">
G
</span>
</div> </div>
</Button> <Switch checked={ai} onCheckedChange={setAi} />
<Button variant="ghost" className="w-full justify-start text-sm text-muted-foreground font-normal h-8 px-2 mb-2" disabled aria-disabled="true" style={{ opacity: 1 }}>
<MessageSquareMore className="h-4 w-4 mr-2 text-indigo-500 opacity-70" />
AI Chat
<div className="ml-auto">
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground">
<span className="text-xs"></span>L
</kbd>
</div> </div>
</Button> {/* <Button className="w-full">
<MonitorPlay className="w-4 h-4 mr-2" /> Run
</Button> */}
</div> </div>
</div> </div>
); );

View File

@ -1,32 +0,0 @@
import React from 'react';
const LoadingDots: React.FC = () => {
return (
<span className="loading-dots">
<span className="dot">.</span>
<span className="dot">.</span>
<span className="dot">.</span>
<style jsx>{`
.loading-dots {
display: inline-block;
font-size: 24px;
}
.dot {
opacity: 0;
animation: showHideDot 1.5s ease-in-out infinite;
}
.dot:nth-child(1) { animation-delay: 0s; }
.dot:nth-child(2) { animation-delay: 0.5s; }
.dot:nth-child(3) { animation-delay: 1s; }
@keyframes showHideDot {
0% { opacity: 0; }
50% { opacity: 1; }
100% { opacity: 0; }
}
`}</style>
</span>
);
};
export default LoadingDots;

View File

@ -22,7 +22,6 @@ const buttonVariants = cva(
}, },
size: { size: {
default: "h-9 px-4 py-2", default: "h-9 px-4 py-2",
xs: "h-6 px-2.5 py-1.5 rounded-sm text-[0.7rem]",
sm: "h-8 rounded-md px-3 text-xs", sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8", lg: "h-10 rounded-md px-8",
icon: "h-9 w-9", icon: "h-9 w-9",

View File

@ -42,13 +42,13 @@ export default function UserButton({ userData }: { userData: User }) {
<div className="py-1.5 px-2 w-full flex flex-col items-start text-sm"> <div className="py-1.5 px-2 w-full flex flex-col items-start text-sm">
<div className="flex items-center"> <div className="flex items-center">
<Sparkles className={`h-4 w-4 mr-2 text-indigo-500`} /> <Sparkles className={`h-4 w-4 mr-2 text-indigo-500`} />
AI Usage: {userData.generations}/1000 AI Usage: {userData.generations}/10
</div> </div>
<div className="rounded-full w-full mt-2 h-2 overflow-hidden bg-secondary"> <div className="rounded-full w-full mt-2 h-2 overflow-hidden bg-secondary">
<div <div
className="h-full bg-indigo-500 rounded-full" className="h-full bg-indigo-500 rounded-full"
style={{ style={{
width: `${(userData.generations * 100) / 1000}%`, width: `${(userData.generations * 100) / 10}%`,
}} }}
/> />
</div> </div>

View File

@ -1,36 +0,0 @@
export const projectTemplates: {
id: string
name: string
icon: string
description: string
disabled: boolean
}[] = [
{
id: "reactjs",
name: "React",
icon: "/project-icons/react.svg",
description: "A JavaScript library for building user interfaces",
disabled: false,
},
{
id: "vanillajs",
name: "HTML/JS",
icon: "/project-icons/more.svg",
description: "More coming soon, feel free to contribute on GitHub",
disabled: false,
},
{
id: "nextjs",
name: "NextJS",
icon: "/project-icons/node.svg",
description: "A JavaScript runtime built on the V8 JavaScript engine",
disabled: false,
},
{
id: "streamlit",
name: "Streamlit",
icon: "/project-icons/python.svg",
description: "A JavaScript runtime built on the V8 JavaScript engine",
disabled: false,
},
]

View File

@ -1,295 +0,0 @@
{
"_coffee": "coffeescript",
"_js": "javascript",
"adp": "tcl",
"al": "perl",
"ant": "xml",
"aw": "php",
"axml": "xml",
"bash": "shell",
"bats": "shell",
"bones": "javascript",
"boot": "clojure",
"builder": "ruby",
"bzl": "python",
"c": "c",
"c++": "cpp",
"cake": "coffeescript",
"cats": "c",
"cc": "cpp",
"ccxml": "xml",
"cfg": "ini",
"cgi": "shell",
"cjsx": "coffeescript",
"cl2": "clojure",
"clixml": "xml",
"clj": "clojure",
"cljc": "clojure",
"cljs.hl": "clojure",
"cljs": "clojure",
"cljscm": "clojure",
"cljx": "clojure",
"coffee": "coffeescript",
"command": "shell",
"cp": "cpp",
"cpp": "cpp",
"cproject": "xml",
"cql": "sql",
"csl": "xml",
"cson": "coffeescript",
"csproj": "xml",
"ct": "xml",
"ctp": "php",
"cxx": "cpp",
"ddl": "sql",
"dfm": "pascal",
"dita": "xml",
"ditamap": "xml",
"ditaval": "xml",
"dll.config": "xml",
"dotsettings": "xml",
"dpr": "pascal",
"ecl": "ecl",
"eclxml": "ecl",
"es": "javascript",
"es6": "javascript",
"ex": "elixir",
"exs": "elixir",
"fcgi": "shell",
"filters": "xml",
"frag": "javascript",
"fsproj": "xml",
"fxml": "xml",
"gemspec": "ruby",
"geojson": "json",
"glade": "xml",
"gml": "xml",
"god": "ruby",
"grxml": "xml",
"gs": "javascript",
"gyp": "python",
"h": "cpp",
"h++": "cpp",
"handlebars": "handlebars",
"hbs": "handlebars",
"hcl": "hcl",
"hh": "cpp",
"hic": "clojure",
"hpp": "cpp",
"htm": "html",
"html.hl": "html",
"html": "html",
"hxx": "cpp",
"iced": "coffeescript",
"idc": "c",
"iml": "xml",
"inc": "sql",
"ini": "ini",
"inl": "cpp",
"ipp": "cpp",
"irbrc": "ruby",
"ivy": "xml",
"j2": "python",
"jake": "javascript",
"jbuilder": "ruby",
"jelly": "xml",
"jinja": "python",
"jinja2": "python",
"js": "javascript",
"jsb": "javascript",
"jscad": "javascript",
"jsfl": "javascript",
"jsm": "javascript",
"json": "json",
"jsproj": "xml",
"jss": "javascript",
"kml": "xml",
"ksh": "shell",
"kt": "kotlin",
"ktm": "kotlin",
"kts": "kotlin",
"launch": "xml",
"lmi": "python",
"lock": "json",
"lpr": "pascal",
"lua": "lua",
"markdown": "markdown",
"md": "markdown",
"mdpolicy": "xml",
"mkd": "markdown",
"mkdn": "markdown",
"mkdown": "markdown",
"mm": "xml",
"mod": "xml",
"mspec": "ruby",
"mustache": "python",
"mxml": "xml",
"njs": "javascript",
"nproj": "xml",
"nse": "lua",
"nuspec": "xml",
"odd": "xml",
"osm": "xml",
"pac": "javascript",
"pas": "pascal",
"pd_lua": "lua",
"perl": "perl",
"ph": "perl",
"php": "php",
"php3": "php",
"php4": "php",
"php5": "php",
"phps": "php",
"phpt": "php",
"pl": "perl",
"plist": "xml",
"pluginspec": "xml",
"plx": "perl",
"pm": "perl",
"pod": "perl",
"podspec": "ruby",
"pp": "pascal",
"prc": "sql",
"prefs": "ini",
"pro": "ini",
"properties": "ini",
"props": "xml",
"ps1": "powershell",
"ps1xml": "xml",
"psc1": "xml",
"psd1": "powershell",
"psgi": "perl",
"psm1": "powershell",
"pt": "xml",
"py": "python",
"pyde": "python",
"pyp": "python",
"pyt": "python",
"pyw": "python",
"r": "r",
"rabl": "ruby",
"rake": "ruby",
"rb": "ruby",
"rbuild": "ruby",
"rbw": "ruby",
"rbx": "ruby",
"rbxs": "lua",
"rd": "r",
"rdf": "xml",
"reek": "yaml",
"rest.txt": "restructuredtext",
"rest": "restructuredtext",
"ron": "markdown",
"rpy": "python",
"rq": "sparql",
"rs.in": "rust",
"rs": "rust",
"rss": "xml",
"rst.txt": "restructuredtext",
"rst": "restructuredtext",
"rsx": "r",
"ru": "ruby",
"ruby": "ruby",
"rviz": "yaml",
"sbt": "scala",
"sc": "scala",
"scala": "scala",
"scm": "scheme",
"scxml": "xml",
"sh.in": "shell",
"sh": "shell",
"sjs": "javascript",
"sld": "scheme",
"sls": "scheme",
"sparql": "sparql",
"sps": "scheme",
"sql": "sql",
"srdf": "xml",
"ss": "scheme",
"ssjs": "javascript",
"st": "html",
"storyboard": "xml",
"sttheme": "xml",
"sublime_metrics": "javascript",
"sublime_session": "javascript",
"sublime-build": "javascript",
"sublime-commands": "javascript",
"sublime-completions": "javascript",
"sublime-keymap": "javascript",
"sublime-macro": "javascript",
"sublime-menu": "javascript",
"sublime-mousemap": "javascript",
"sublime-project": "javascript",
"sublime-settings": "javascript",
"sublime-snippet": "xml",
"sublime-syntax": "yaml",
"sublime-theme": "javascript",
"sublime-workspace": "javascript",
"sv": "systemverilog",
"svh": "systemverilog",
"syntax": "yaml",
"t": "perl",
"tab": "sql",
"tac": "python",
"targets": "xml",
"tcc": "cpp",
"tcl": "tcl",
"tf": "hcl",
"thor": "ruby",
"tm": "tcl",
"tmcommand": "xml",
"tml": "xml",
"tmlanguage": "xml",
"tmpreferences": "xml",
"tmsnippet": "xml",
"tmtheme": "xml",
"tmux": "shell",
"tool": "shell",
"topojson": "json",
"tpp": "cpp",
"ts": "typescript",
"tsx": "typescript",
"udf": "sql",
"ui": "xml",
"urdf": "xml",
"ux": "xml",
"v": "verilog",
"vbproj": "xml",
"vcxproj": "xml",
"veo": "verilog",
"vh": "systemverilog",
"viw": "sql",
"vssettings": "xml",
"vxml": "xml",
"w": "c",
"watchr": "ruby",
"wlua": "lua",
"wsdl": "xml",
"wsf": "xml",
"wsgi": "python",
"wxi": "xml",
"wxl": "xml",
"wxs": "xml",
"x3d": "xml",
"xacro": "xml",
"xaml": "xml",
"xht": "html",
"xhtml": "html",
"xib": "xml",
"xlf": "xml",
"xliff": "xml",
"xmi": "xml",
"xml.dist": "xml",
"xml": "xml",
"xproj": "xml",
"xpy": "python",
"xsd": "xml",
"xsjs": "javascript",
"xsjslib": "javascript",
"xul": "xml",
"yaml-tmlanguage": "yaml",
"yaml": "yaml",
"yml": "yaml",
"zcml": "xml",
"zsh": "shell"
}

View File

@ -1,99 +0,0 @@
import * as monaco from "monaco-editor"
export function parseTSConfigToMonacoOptions(
tsconfig: any
): monaco.languages.typescript.CompilerOptions {
const compilerOptions: monaco.languages.typescript.CompilerOptions = {}
// Map tsconfig options to Monaco CompilerOptions
if (tsconfig.strict) compilerOptions.strict = tsconfig.strict
if (tsconfig.target) compilerOptions.target = mapScriptTarget(tsconfig.target)
if (tsconfig.module) compilerOptions.module = mapModule(tsconfig.module)
if (tsconfig.lib) compilerOptions.lib = tsconfig.lib
if (tsconfig.allowJs) compilerOptions.allowJs = tsconfig.allowJs
if (tsconfig.checkJs) compilerOptions.checkJs = tsconfig.checkJs
if (tsconfig.jsx) compilerOptions.jsx = mapJSX(tsconfig.jsx)
if (tsconfig.declaration) compilerOptions.declaration = tsconfig.declaration
if (tsconfig.declarationMap)
compilerOptions.declarationMap = tsconfig.declarationMap
if (tsconfig.sourceMap) compilerOptions.sourceMap = tsconfig.sourceMap
if (tsconfig.outFile) compilerOptions.outFile = tsconfig.outFile
if (tsconfig.outDir) compilerOptions.outDir = tsconfig.outDir
if (tsconfig.removeComments)
compilerOptions.removeComments = tsconfig.removeComments
if (tsconfig.noEmit) compilerOptions.noEmit = tsconfig.noEmit
if (tsconfig.noEmitOnError)
compilerOptions.noEmitOnError = tsconfig.noEmitOnError
return compilerOptions
}
function mapScriptTarget(
target: string
): monaco.languages.typescript.ScriptTarget {
const targetMap: { [key: string]: monaco.languages.typescript.ScriptTarget } =
{
es3: monaco.languages.typescript.ScriptTarget.ES3,
es5: monaco.languages.typescript.ScriptTarget.ES5,
es6: monaco.languages.typescript.ScriptTarget.ES2015,
es2015: monaco.languages.typescript.ScriptTarget.ES2015,
es2016: monaco.languages.typescript.ScriptTarget.ES2016,
es2017: monaco.languages.typescript.ScriptTarget.ES2017,
es2018: monaco.languages.typescript.ScriptTarget.ES2018,
es2019: monaco.languages.typescript.ScriptTarget.ES2019,
es2020: monaco.languages.typescript.ScriptTarget.ES2020,
esnext: monaco.languages.typescript.ScriptTarget.ESNext,
}
if (typeof target !== "string") {
return monaco.languages.typescript.ScriptTarget.Latest
}
return (
targetMap[target?.toLowerCase()] ||
monaco.languages.typescript.ScriptTarget.Latest
)
}
function mapModule(module: string): monaco.languages.typescript.ModuleKind {
const moduleMap: { [key: string]: monaco.languages.typescript.ModuleKind } = {
none: monaco.languages.typescript.ModuleKind.None,
commonjs: monaco.languages.typescript.ModuleKind.CommonJS,
amd: monaco.languages.typescript.ModuleKind.AMD,
umd: monaco.languages.typescript.ModuleKind.UMD,
system: monaco.languages.typescript.ModuleKind.System,
es6: monaco.languages.typescript.ModuleKind.ES2015,
es2015: monaco.languages.typescript.ModuleKind.ES2015,
esnext: monaco.languages.typescript.ModuleKind.ESNext,
}
if (typeof module !== "string") {
return monaco.languages.typescript.ModuleKind.ESNext
}
return (
moduleMap[module.toLowerCase()] ||
monaco.languages.typescript.ModuleKind.ESNext
)
}
function mapJSX(jsx: string): monaco.languages.typescript.JsxEmit {
const jsxMap: { [key: string]: monaco.languages.typescript.JsxEmit } = {
preserve: monaco.languages.typescript.JsxEmit.Preserve,
react: monaco.languages.typescript.JsxEmit.React,
"react-native": monaco.languages.typescript.JsxEmit.ReactNative,
}
return jsxMap[jsx.toLowerCase()] || monaco.languages.typescript.JsxEmit.React
}
// Example usage:
const tsconfigJSON = {
compilerOptions: {
strict: true,
target: "ES2020",
module: "ESNext",
lib: ["DOM", "ES2020"],
jsx: "react",
sourceMap: true,
outDir: "./dist",
},
}
const monacoOptions = parseTSConfigToMonacoOptions(tsconfigJSON.compilerOptions)
console.log(monacoOptions)

View File

@ -2,19 +2,18 @@ import { type ClassValue, clsx } from "clsx"
// import { toast } from "sonner" // import { toast } from "sonner"
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge"
import { Sandbox, TFile, TFolder } from "./types" import { Sandbox, TFile, TFolder } from "./types"
import fileExtToLang from "./file-extension-to-language.json"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
} }
export function processFileType(file: string) { export function processFileType(file: string) {
const extension = file.split(".").pop() const ending = file.split(".").pop()
const fileExtToLangMap = fileExtToLang as Record<string, string>
if (extension && fileExtToLangMap[extension]) {
return fileExtToLangMap[extension]
}
if (ending === "ts" || ending === "tsx") return "typescript"
if (ending === "js" || ending === "jsx") return "javascript"
if (ending) return ending
return "plaintext" return "plaintext"
} }
@ -63,38 +62,12 @@ export function addNew(
} }
} }
export function debounce<T extends (...args: any[]) => void>( export function debounce<T extends (...args: any[]) => void>(func: T, wait: number): T {
func: T, let timeout: NodeJS.Timeout | null = null;
wait: number
): T {
let timeout: NodeJS.Timeout | null = null
return function (...args: Parameters<T>) { return function (...args: Parameters<T>) {
if (timeout) { if (timeout) {
clearTimeout(timeout) clearTimeout(timeout);
} }
timeout = setTimeout(() => func(...args), wait) timeout = setTimeout(() => func(...args), wait);
} as T } as T;
}
// Deep merge utility function
export const deepMerge = (target: any, source: any) => {
const output = { ...target }
if (isObject(target) && isObject(source)) {
Object.keys(source).forEach((key) => {
if (isObject(source[key])) {
if (!(key in target)) {
Object.assign(output, { [key]: source[key] })
} else {
output[key] = deepMerge(target[key], source[key])
}
} else {
Object.assign(output, { [key]: source[key] })
}
})
}
return output
}
const isObject = (item: any) => {
return item && typeof item === "object" && !Array.isArray(item)
} }

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,6 @@
"@atlaskit/pragmatic-drag-and-drop": "^1.1.7", "@atlaskit/pragmatic-drag-and-drop": "^1.1.7",
"@clerk/nextjs": "^4.29.12", "@clerk/nextjs": "^4.29.12",
"@clerk/themes": "^1.7.12", "@clerk/themes": "^1.7.12",
"@codemirror/lang-javascript": "^6.2.2",
"@hookform/resolvers": "^3.3.4", "@hookform/resolvers": "^3.3.4",
"@liveblocks/client": "^1.12.0", "@liveblocks/client": "^1.12.0",
"@liveblocks/node": "^1.12.0", "@liveblocks/node": "^1.12.0",
@ -31,8 +30,6 @@
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-switch": "^1.0.3",
"@react-three/fiber": "^8.16.6", "@react-three/fiber": "^8.16.6",
"@uiw/codemirror-theme-vscode": "^4.23.5",
"@uiw/react-codemirror": "^4.23.5",
"@vercel/analytics": "^1.2.2", "@vercel/analytics": "^1.2.2",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
@ -49,10 +46,7 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18", "react-dom": "^18",
"react-hook-form": "^7.51.3", "react-hook-form": "^7.51.3",
"react-markdown": "^9.0.1",
"react-resizable-panels": "^2.0.16", "react-resizable-panels": "^2.0.16",
"react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^4.0.0",
"socket.io-client": "^4.7.5", "socket.io-client": "^4.7.5",
"sonner": "^1.4.41", "sonner": "^1.4.41",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.3.0",
@ -65,11 +59,9 @@
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@types/estree": "^1.0.6",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/three": "^0.164.0", "@types/three": "^0.164.0",
"autoprefixer": "^10.0.1", "autoprefixer": "^10.0.1",
"postcss": "^8", "postcss": "^8",

View File

@ -1,3 +0,0 @@
declare module 'react-syntax-highlighter';
declare module 'react-syntax-highlighter/dist/esm/styles/prism';

View File

@ -1,7 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"], "lib": ["dom", "dom.iterable", "esnext"],
"types": ["node"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,