Compare commits
21 Commits
fix/migrat
...
sync-conta
Author | SHA1 | Date | |
---|---|---|---|
023b3bdc5e | |||
13be78dee8 | |||
7a00d24ab9 | |||
69b1287349 | |||
c94678c430 | |||
585dcb469e | |||
2f88ff6d58 | |||
0509716f34 | |||
06118e98e9 | |||
4ebd6dea96 | |||
8921cd83bb | |||
45097e0f20 | |||
62e6d64a52 | |||
0c6b2b0dfb | |||
31d74ddc2d | |||
62311faf51 | |||
208d17879f | |||
0067dc8c0c | |||
4fe749daf2 | |||
bf79893dfa | |||
47324f15bf |
216
backend/ai/package-lock.json
generated
216
backend/ai/package-lock.json
generated
@ -7,6 +7,9 @@
|
||||
"": {
|
||||
"name": "ai",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.27.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/vitest-pool-workers": "^0.1.0",
|
||||
"@cloudflare/workers-types": "^4.20240512.0",
|
||||
@ -15,6 +18,28 @@
|
||||
"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": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.2.tgz",
|
||||
@ -861,11 +886,19 @@
|
||||
"version": "20.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz",
|
||||
"integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"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": {
|
||||
"version": "1.3.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz",
|
||||
@ -944,6 +977,17 @@
|
||||
"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": {
|
||||
"version": "8.11.3",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
|
||||
@ -965,6 +1009,17 @@
|
||||
"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": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
@ -1008,6 +1063,11 @@
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@ -1141,6 +1201,17 @@
|
||||
"integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==",
|
||||
"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": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz",
|
||||
@ -1214,6 +1285,14 @@
|
||||
"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": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.3.tgz",
|
||||
@ -1287,6 +1366,14 @@
|
||||
"@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": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
|
||||
@ -1334,6 +1421,36 @@
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
@ -1452,6 +1569,14 @@
|
||||
"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": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
@ -1610,6 +1735,25 @@
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
|
||||
@ -1675,8 +1819,7 @@
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"dev": true
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/mustache": {
|
||||
"version": "4.2.0",
|
||||
@ -1705,6 +1848,43 @@
|
||||
"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": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
@ -2222,6 +2402,11 @@
|
||||
"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": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-json-schema-generator/-/ts-json-schema-generator-1.5.1.tgz",
|
||||
@ -2292,8 +2477,7 @@
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.2.11",
|
||||
@ -2827,6 +3011,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
@ -15,5 +15,8 @@
|
||||
"typescript": "^5.0.4",
|
||||
"vitest": "1.3.0",
|
||||
"wrangler": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.27.2"
|
||||
}
|
||||
}
|
||||
|
@ -1,43 +1,77 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk";
|
||||
|
||||
export interface Env {
|
||||
AI: any
|
||||
ANTHROPIC_API_KEY: string;
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request, env): Promise<Response> {
|
||||
if (request.method !== "GET") {
|
||||
return new Response("Method Not Allowed", { status: 405 })
|
||||
}
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
if (request.method !== "GET") {
|
||||
return new Response("Method Not Allowed", { status: 405 });
|
||||
}
|
||||
|
||||
const url = new URL(request.url)
|
||||
const fileName = url.searchParams.get("fileName")
|
||||
const instructions = url.searchParams.get("instructions")
|
||||
const line = url.searchParams.get("line")
|
||||
const code = url.searchParams.get("code")
|
||||
const url = new URL(request.url);
|
||||
// const fileName = url.searchParams.get("fileName");
|
||||
// const line = url.searchParams.get("line");
|
||||
const instructions = url.searchParams.get("instructions");
|
||||
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}.`,
|
||||
},
|
||||
{
|
||||
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
|
||||
|
||||
${code}`,
|
||||
},
|
||||
],
|
||||
})
|
||||
const prompt = `
|
||||
Make the following changes to the code below:
|
||||
- ${instructions}
|
||||
|
||||
return new Response(JSON.stringify(response))
|
||||
},
|
||||
} satisfies ExportedHandler<Env>
|
||||
Return the complete code chunk. Do not refer to other code files. Do not add code before or after the chunk. Start your reponse with \`\`\`, and end with \`\`\`. Do not include any other text.
|
||||
|
||||
\`\`\`
|
||||
${code}
|
||||
\`\`\`
|
||||
`;
|
||||
console.log(prompt);
|
||||
|
||||
try {
|
||||
const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY });
|
||||
|
||||
interface TextBlock {
|
||||
type: "text";
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface ToolUseBlock {
|
||||
type: "tool_use";
|
||||
tool_use: {
|
||||
// Add properties if needed
|
||||
};
|
||||
}
|
||||
|
||||
type ContentBlock = TextBlock | ToolUseBlock;
|
||||
|
||||
function getTextContent(content: ContentBlock[]): string {
|
||||
for (const block of content) {
|
||||
if (block.type === "text") {
|
||||
return block.text;
|
||||
}
|
||||
}
|
||||
return "No text content found";
|
||||
}
|
||||
|
||||
const response = await anthropic.messages.create({
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
max_tokens: 1024,
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
});
|
||||
|
||||
const message = response.content as ContentBlock[];
|
||||
const textBlockContent = getTextContent(message);
|
||||
|
||||
const pattern = /```[a-zA-Z]*\n([\s\S]*?)\n```/;
|
||||
const match = textBlockContent.match(pattern);
|
||||
|
||||
const codeContent = match ? match[1] : "Error: Could not extract code.";
|
||||
|
||||
return new Response(JSON.stringify({ "response": codeContent }))
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
return new Response("Internal Server Error", { status: 500 });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -6,10 +6,15 @@ import { createServer } from "http";
|
||||
import { Server } from "socket.io";
|
||||
import { DokkuClient } from "./DokkuClient";
|
||||
import { SecureGitClient, FileData } from "./SecureGitClient";
|
||||
import fs from "fs";
|
||||
import fs, { readFile } from "fs";
|
||||
|
||||
import { z } from "zod";
|
||||
import { User } from "./types";
|
||||
import {
|
||||
TFile,
|
||||
TFileData,
|
||||
TFolder,
|
||||
User
|
||||
} from "./types";
|
||||
import {
|
||||
createFile,
|
||||
deleteFile,
|
||||
@ -21,7 +26,7 @@ import {
|
||||
} from "./fileoperations";
|
||||
import { LockManager } from "./utils";
|
||||
|
||||
import { Sandbox, Filesystem } from "e2b";
|
||||
import { Sandbox, Filesystem, FilesystemEvent, EntryInfo } from "e2b";
|
||||
|
||||
import { Terminal } from "./Terminal"
|
||||
|
||||
@ -34,6 +39,18 @@ import {
|
||||
saveFileRL,
|
||||
} 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
|
||||
});
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app: Express = express();
|
||||
@ -55,14 +72,14 @@ const terminals: Record<string, Terminal> = {};
|
||||
|
||||
const dirName = "/home/user";
|
||||
|
||||
const moveFile = async (
|
||||
filesystem: Filesystem,
|
||||
filePath: string,
|
||||
newFilePath: string
|
||||
) => {
|
||||
const fileContents = await filesystem.read(filePath);
|
||||
await filesystem.write(newFilePath, fileContents);
|
||||
await filesystem.remove(filePath);
|
||||
const moveFile = async (filesystem: Filesystem, filePath: string, newFilePath: string) => {
|
||||
try {
|
||||
const fileContents = await filesystem.read(filePath);
|
||||
await filesystem.write(newFilePath, fileContents);
|
||||
await filesystem.remove(filePath);
|
||||
} catch (e) {
|
||||
console.error(`Error moving file from ${filePath} to ${newFilePath}:`, e);
|
||||
}
|
||||
};
|
||||
|
||||
io.use(async (socket, next) => {
|
||||
@ -157,11 +174,12 @@ io.on("connection", async (socket) => {
|
||||
}
|
||||
}
|
||||
|
||||
await lockManager.acquireLock(data.sandboxId, async () => {
|
||||
const createdContainer = await lockManager.acquireLock(data.sandboxId, async () => {
|
||||
try {
|
||||
if (!containers[data.sandboxId]) {
|
||||
containers[data.sandboxId] = await Sandbox.create({ timeoutMs: 1200000 });
|
||||
console.log("Created container ", data.sandboxId);
|
||||
return true;
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(`Error creating container ${data.sandboxId}:`, e);
|
||||
@ -169,27 +187,171 @@ io.on("connection", async (socket) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Change the owner of the project directory to user
|
||||
const fixPermissions = async () => {
|
||||
await containers[data.sandboxId].commands.run(
|
||||
`sudo chown -R user "${path.join(dirName, "projects", data.sandboxId)}"`
|
||||
);
|
||||
};
|
||||
|
||||
const sandboxFiles = await getSandboxFiles(data.sandboxId);
|
||||
sandboxFiles.fileData.forEach(async (file) => {
|
||||
const filePath = path.join(dirName, file.id);
|
||||
const projectDirectory = path.join(dirName, "projects", data.sandboxId);
|
||||
const containerFiles = containers[data.sandboxId].files;
|
||||
|
||||
// Change the owner of the project directory to user
|
||||
const fixPermissions = async (projectDirectory: string) => {
|
||||
try {
|
||||
await containers[data.sandboxId].files.makeDir(
|
||||
path.dirname(filePath)
|
||||
await containers[data.sandboxId].commands.run(
|
||||
`sudo chown -R user "${projectDirectory}"`
|
||||
);
|
||||
} catch (e: any) {
|
||||
console.log("Failed to create directory: " + e);
|
||||
console.log("Failed to fix permissions: " + e);
|
||||
}
|
||||
await containers[data.sandboxId].files.write(filePath, file.data);
|
||||
});
|
||||
fixPermissions();
|
||||
};
|
||||
|
||||
// Check if the given path is a directory
|
||||
const isDirectory = async (projectDirectory: string): Promise<boolean> => {
|
||||
try {
|
||||
const result = await containers[data.sandboxId].commands.run(
|
||||
`[ -d "${projectDirectory}" ] && echo "true" || echo "false"`
|
||||
);
|
||||
return result.stdout.trim() === "true";
|
||||
} 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.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);
|
||||
|
||||
// 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) => {
|
||||
try {
|
||||
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.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);
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Error watching filesystem:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
// Watch the project directory
|
||||
await watchDirectory(projectDirectory);
|
||||
|
||||
// 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);
|
||||
await watchDirectory(item.path);
|
||||
}
|
||||
}))
|
||||
|
||||
socket.emit("loaded", sandboxFiles.files);
|
||||
|
||||
socket.on("getFile", (fileId: string, callback) => {
|
||||
@ -243,7 +405,7 @@ io.on("connection", async (socket) => {
|
||||
path.join(dirName, file.id),
|
||||
body
|
||||
);
|
||||
fixPermissions();
|
||||
fixPermissions(projectDirectory);
|
||||
} catch (e: any) {
|
||||
console.error("Error saving file:", e);
|
||||
io.emit("error", `Error: file saving. ${e.message ?? e}`);
|
||||
@ -265,7 +427,7 @@ io.on("connection", async (socket) => {
|
||||
path.join(dirName, fileId),
|
||||
path.join(dirName, newFileId)
|
||||
);
|
||||
fixPermissions();
|
||||
fixPermissions(projectDirectory);
|
||||
|
||||
file.id = newFileId;
|
||||
|
||||
@ -358,7 +520,7 @@ io.on("connection", async (socket) => {
|
||||
path.join(dirName, id),
|
||||
""
|
||||
);
|
||||
fixPermissions();
|
||||
fixPermissions(projectDirectory);
|
||||
|
||||
sandboxFiles.files.push({
|
||||
id,
|
||||
@ -424,7 +586,7 @@ io.on("connection", async (socket) => {
|
||||
path.join(dirName, fileId),
|
||||
path.join(dirName, newFileId)
|
||||
);
|
||||
fixPermissions();
|
||||
fixPermissions(projectDirectory);
|
||||
await renameFile(fileId, newFileId, file.data);
|
||||
} catch (e: any) {
|
||||
console.error("Error renaming folder:", e);
|
||||
@ -612,7 +774,7 @@ io.on("connection", async (socket) => {
|
||||
|
||||
// Generate code from cloudflare workers AI
|
||||
const generateCodePromise = fetch(
|
||||
`${process.env.AI_WORKER_URL}/api?fileName=${fileName}&code=${code}&line=${line}&instructions=${instructions}`,
|
||||
`${process.env.AI_WORKER_URL}/api?fileName=${encodeURIComponent(fileName)}&code=${encodeURIComponent(code)}&line=${encodeURIComponent(line)}&instructions=${encodeURIComponent(instructions)}`,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
32
backend/storage/package-lock.json
generated
32
backend/storage/package-lock.json
generated
@ -8,6 +8,7 @@
|
||||
"name": "storage",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"p-limit": "^6.1.0",
|
||||
"zod": "^3.23.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -894,6 +895,21 @@
|
||||
"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": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.0.tgz",
|
||||
@ -1766,12 +1782,11 @@
|
||||
}
|
||||
},
|
||||
"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,
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.1.0.tgz",
|
||||
"integrity": "sha512-H0jc0q1vOzlEk0TqAKXKZxdl7kX3OFUzCnNVUnq5Pc3DGo0kpeaMuPqxQn235HibwBEb0/pm9dgKTjXy66fBkg==",
|
||||
"dependencies": {
|
||||
"yocto-queue": "^1.0.0"
|
||||
"yocto-queue": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@ -2970,10 +2985,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
|
||||
"integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==",
|
||||
"dev": true,
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz",
|
||||
"integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
},
|
||||
|
@ -17,6 +17,7 @@
|
||||
"wrangler": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"p-limit": "^6.1.0",
|
||||
"zod": "^3.23.4"
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { z } from "zod"
|
||||
import pLimit from 'p-limit';
|
||||
|
||||
export interface Env {
|
||||
R2: R2Bucket
|
||||
Templates: R2Bucket
|
||||
KEY: string
|
||||
}
|
||||
|
||||
@ -144,17 +146,18 @@ export default {
|
||||
|
||||
console.log(`Copying template: ${type}`);
|
||||
|
||||
const templateDirectory = `templates/${type}`;
|
||||
|
||||
// List all objects under the directory
|
||||
const { objects } = await env.R2.list({ prefix: templateDirectory });
|
||||
const { objects } = await env.Templates.list({ prefix: type });
|
||||
|
||||
// Copy each object to the new directory
|
||||
for (const { key } of objects) {
|
||||
const destinationKey = key.replace(templateDirectory, `projects/${sandboxId}`);
|
||||
const fileBody = await env.R2.get(key).then(res => res?.body ?? "");
|
||||
// Copy each object to the new directory with a 5 concurrency limit
|
||||
const limit = pLimit(5);
|
||||
await Promise.all(objects.map(({ key }) =>
|
||||
limit(async () => {
|
||||
const destinationKey = key.replace(type, `projects/${sandboxId}`);
|
||||
const fileBody = await env.Templates.get(key).then(res => res?.body ?? "");
|
||||
await env.R2.put(destinationKey, fileBody);
|
||||
}
|
||||
})
|
||||
));
|
||||
|
||||
return success
|
||||
} else {
|
||||
|
@ -93,7 +93,7 @@ export default function NewProjectModal({
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
}) {
|
||||
const [selected, setSelected] = useState("react")
|
||||
const [selected, setSelected] = useState("reactjs")
|
||||
const [loading, setLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { Button } from "../ui/button"
|
||||
import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react"
|
||||
import { Socket } from "socket.io-client"
|
||||
@ -59,7 +59,7 @@ export default function GenerateInput({
|
||||
}: {
|
||||
regenerate?: boolean
|
||||
}) => {
|
||||
if (user.generations >= 10) {
|
||||
if (user.generations >= 1000) {
|
||||
toast.error("You reached the maximum # of generations.")
|
||||
return
|
||||
}
|
||||
@ -84,6 +84,13 @@ export default function GenerateInput({
|
||||
}
|
||||
)
|
||||
}
|
||||
const handleGenerateForm = useCallback(
|
||||
(e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
handleGenerate({ regenerate: false })
|
||||
},
|
||||
[input, currentPrompt]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (code) {
|
||||
@ -93,9 +100,23 @@ export default function GenerateInput({
|
||||
}
|
||||
}, [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 (
|
||||
<div className="w-full pr-4 space-y-2">
|
||||
<div className="flex items-center font-sans space-x-2">
|
||||
<form
|
||||
onSubmit={handleGenerateForm}
|
||||
className="flex items-center font-sans space-x-2"
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
style={{
|
||||
@ -109,8 +130,8 @@ export default function GenerateInput({
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
type="submit"
|
||||
disabled={loading.generate || loading.regenerate || input === ""}
|
||||
onClick={() => handleGenerate({})}
|
||||
>
|
||||
{loading.generate ? (
|
||||
<>
|
||||
@ -126,13 +147,14 @@ export default function GenerateInput({
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="smIcon"
|
||||
className="bg-transparent shrink-0 border-muted-foreground"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
{expanded ? (
|
||||
<>
|
||||
<div className="rounded-md border border-muted-foreground w-full h-28 overflow-y-scroll p-2">
|
||||
|
@ -1,10 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import { SetStateAction, useCallback, useEffect, useRef, useState } from "react"
|
||||
import monaco from "monaco-editor"
|
||||
import * as monaco from "monaco-editor"
|
||||
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
|
||||
import { toast } from "sonner"
|
||||
import { useClerk } from "@clerk/nextjs"
|
||||
import { AnimatePresence, motion } from "framer-motion"
|
||||
|
||||
import * as Y from "yjs"
|
||||
import LiveblocksProvider from "@liveblocks/yjs"
|
||||
@ -17,7 +18,7 @@ import {
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "@/components/ui/resizable"
|
||||
import { FileJson, Loader2, TerminalSquare } from "lucide-react"
|
||||
import { FileJson, Loader2, Sparkles, TerminalSquare } from "lucide-react"
|
||||
import Tab from "../ui/tab"
|
||||
import Sidebar from "./sidebar"
|
||||
import GenerateInput from "./generate"
|
||||
@ -30,8 +31,10 @@ import Loading from "./loading"
|
||||
import PreviewWindow from "./preview"
|
||||
import Terminals from "./terminals"
|
||||
import { ImperativePanelHandle } from "react-resizable-panels"
|
||||
import { PreviewProvider, usePreview } from '@/context/PreviewContext';
|
||||
import { PreviewProvider, usePreview } from "@/context/PreviewContext"
|
||||
import { useSocket } from "@/context/SocketContext"
|
||||
import { Button } from "../ui/button"
|
||||
import React from "react"
|
||||
|
||||
export default function CodeEditor({
|
||||
userData,
|
||||
@ -40,9 +43,8 @@ export default function CodeEditor({
|
||||
userData: User
|
||||
sandboxData: Sandbox
|
||||
}) {
|
||||
|
||||
//SocketContext functions and effects
|
||||
const { socket, setUserAndSandboxId } = useSocket();
|
||||
const { socket, setUserAndSandboxId } = useSocket()
|
||||
|
||||
useEffect(() => {
|
||||
// Ensure userData.id and sandboxData.id are available before attempting to connect
|
||||
@ -50,10 +52,10 @@ export default function CodeEditor({
|
||||
// Check if the socket is not initialized or not connected
|
||||
if (!socket || (socket && !socket.connected)) {
|
||||
// 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])
|
||||
|
||||
//Preview Button state
|
||||
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
|
||||
@ -76,7 +78,6 @@ export default function CodeEditor({
|
||||
useState<monaco.editor.IStandaloneCodeEditor>()
|
||||
|
||||
// AI Copilot state
|
||||
const [ai, setAi] = useState(false)
|
||||
const [generate, setGenerate] = useState<{
|
||||
show: boolean
|
||||
id: string
|
||||
@ -89,7 +90,8 @@ export default function CodeEditor({
|
||||
options: monaco.editor.IModelDeltaDecoration[]
|
||||
instance: monaco.editor.IEditorDecorationsCollection | undefined
|
||||
}>({ options: [], instance: undefined })
|
||||
|
||||
const [isSelected, setIsSelected] = useState(false)
|
||||
const [showSuggestion, setShowSuggestion] = useState(false)
|
||||
// Terminal state
|
||||
const [terminals, setTerminals] = useState<
|
||||
{
|
||||
@ -99,13 +101,13 @@ export default function CodeEditor({
|
||||
>([])
|
||||
|
||||
// Preview state
|
||||
const [previewURL, setPreviewURL] = useState<string>("");
|
||||
const [previewURL, setPreviewURL] = useState<string>("")
|
||||
|
||||
const loadPreviewURL = (url: string) => {
|
||||
// This will cause a reload if previewURL changed.
|
||||
setPreviewURL(url);
|
||||
setPreviewURL(url)
|
||||
// If the URL didn't change, still reload the preview.
|
||||
previewWindowRef.current?.refreshIframe();
|
||||
previewWindowRef.current?.refreshIframe()
|
||||
}
|
||||
|
||||
const isOwner = sandboxData.userId === userData.id
|
||||
@ -118,23 +120,29 @@ export default function CodeEditor({
|
||||
|
||||
// Liveblocks providers map to prevent reinitializing providers
|
||||
type ProviderData = {
|
||||
provider: LiveblocksProvider<never, never, never, never>;
|
||||
yDoc: Y.Doc;
|
||||
yText: Y.Text;
|
||||
binding?: MonacoBinding;
|
||||
onSync: (isSynced: boolean) => void;
|
||||
};
|
||||
const providersMap = useRef(new Map<string, ProviderData>());
|
||||
provider: LiveblocksProvider<never, never, never, never>
|
||||
yDoc: Y.Doc
|
||||
yText: Y.Text
|
||||
binding?: MonacoBinding
|
||||
onSync: (isSynced: boolean) => void
|
||||
}
|
||||
const providersMap = useRef(new Map<string, ProviderData>())
|
||||
|
||||
// Refs for libraries / features
|
||||
const editorContainerRef = useRef<HTMLDivElement>(null)
|
||||
const monacoRef = useRef<typeof monaco | null>(null)
|
||||
const generateRef = useRef<HTMLDivElement>(null)
|
||||
const suggestionRef = useRef<HTMLDivElement>(null)
|
||||
const generateWidgetRef = useRef<HTMLDivElement>(null)
|
||||
const previewPanelRef = useRef<ImperativePanelHandle>(null)
|
||||
const editorPanelRef = useRef<ImperativePanelHandle>(null)
|
||||
const previewWindowRef = useRef<{ refreshIframe: () => void }>(null)
|
||||
|
||||
const debouncedSetIsSelected = useRef(
|
||||
debounce((value: boolean) => {
|
||||
setIsSelected(value)
|
||||
}, 800) //
|
||||
).current
|
||||
// Pre-mount editor keybindings
|
||||
const handleEditorWillMount: BeforeMount = (monaco) => {
|
||||
monaco.editor.addKeybindingRules([
|
||||
@ -151,6 +159,13 @@ export default function CodeEditor({
|
||||
monacoRef.current = monaco
|
||||
|
||||
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
|
||||
if (lineNumber === cursorLine) return
|
||||
setCursorLine(lineNumber)
|
||||
@ -204,28 +219,62 @@ export default function CodeEditor({
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Generate widget effect
|
||||
useEffect(() => {
|
||||
if (!ai) {
|
||||
setGenerate((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
show: false,
|
||||
}
|
||||
})
|
||||
return
|
||||
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
|
||||
}
|
||||
if (generate.show) {
|
||||
editorRef?.changeViewZones(function (changeAccessor) {
|
||||
if (!generateRef.current) return
|
||||
const id = changeAccessor.addZone({
|
||||
afterLineNumber: cursorLine,
|
||||
heightInLines: 3,
|
||||
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
|
||||
useEffect(() => {
|
||||
if (generate.show) {
|
||||
setShowSuggestion(false)
|
||||
editorRef?.changeViewZones(function (changeAccessor) {
|
||||
if (!generateRef.current) return
|
||||
if (!generate.id) {
|
||||
const id = changeAccessor.addZone({
|
||||
afterLineNumber: cursorLine,
|
||||
heightInLines: 3,
|
||||
domNode: generateRef.current,
|
||||
})
|
||||
setGenerate((prev) => {
|
||||
return { ...prev, id, line: cursorLine }
|
||||
})
|
||||
}
|
||||
setGenerate((prev) => {
|
||||
return { ...prev, id, line: cursorLine }
|
||||
return { ...prev, line: cursorLine }
|
||||
})
|
||||
})
|
||||
|
||||
@ -286,6 +335,41 @@ export default function CodeEditor({
|
||||
})
|
||||
}
|
||||
}, [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
|
||||
useEffect(() => {
|
||||
@ -293,8 +377,6 @@ export default function CodeEditor({
|
||||
decorations.instance?.clear()
|
||||
}
|
||||
|
||||
if (!ai) return
|
||||
|
||||
const model = editorRef?.getModel()
|
||||
const line = model?.getLineContent(cursorLine)
|
||||
|
||||
@ -325,27 +407,27 @@ export default function CodeEditor({
|
||||
prev.map((tab) =>
|
||||
tab.id === activeFileId ? { ...tab, saved: true } : tab
|
||||
)
|
||||
);
|
||||
console.log(`Saving file...${activeFileId}`);
|
||||
console.log(`Saving file...${value}`);
|
||||
socket?.emit("saveFile", activeFileId, value);
|
||||
)
|
||||
console.log(`Saving file...${activeFileId}`)
|
||||
console.log(`Saving file...${value}`)
|
||||
socket?.emit("saveFile", activeFileId, value)
|
||||
}, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000),
|
||||
[socket]
|
||||
);
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
debouncedSaveData(editorRef?.getValue(), activeFileId);
|
||||
e.preventDefault()
|
||||
debouncedSaveData(editorRef?.getValue(), activeFileId)
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", down);
|
||||
}
|
||||
document.addEventListener("keydown", down)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", down);
|
||||
};
|
||||
}, [activeFileId, tabs, debouncedSaveData]);
|
||||
document.removeEventListener("keydown", down)
|
||||
}
|
||||
}, [activeFileId, tabs, debouncedSaveData])
|
||||
|
||||
// Liveblocks live collaboration setup effect
|
||||
useEffect(() => {
|
||||
@ -354,13 +436,13 @@ export default function CodeEditor({
|
||||
|
||||
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.
|
||||
if (!providersMap.current.has(tab.id)) {
|
||||
const yDoc = new Y.Doc();
|
||||
const yText = yDoc.getText(tab.id);
|
||||
const yProvider = new LiveblocksProvider(room, yDoc);
|
||||
const yDoc = new Y.Doc()
|
||||
const yText = yDoc.getText(tab.id)
|
||||
const yProvider = new LiveblocksProvider(room, yDoc)
|
||||
|
||||
// Inserts the file content into the editor once when the tab is changed.
|
||||
const onSync = (isSynced: boolean) => {
|
||||
@ -381,12 +463,11 @@ export default function CodeEditor({
|
||||
yProvider.on("sync", onSync)
|
||||
|
||||
// Save the provider to the map.
|
||||
providerData = { provider: yProvider, yDoc, yText, onSync };
|
||||
providersMap.current.set(tab.id, providerData);
|
||||
|
||||
providerData = { provider: yProvider, yDoc, yText, onSync }
|
||||
providersMap.current.set(tab.id, providerData)
|
||||
} else {
|
||||
// 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(
|
||||
@ -394,21 +475,21 @@ export default function CodeEditor({
|
||||
model,
|
||||
new Set([editorRef]),
|
||||
providerData.provider.awareness as unknown as Awareness
|
||||
);
|
||||
)
|
||||
|
||||
providerData.binding = binding;
|
||||
setProvider(providerData.provider);
|
||||
providerData.binding = binding
|
||||
setProvider(providerData.provider)
|
||||
|
||||
return () => {
|
||||
// Cleanup logic
|
||||
if (binding) {
|
||||
binding.destroy();
|
||||
binding.destroy()
|
||||
}
|
||||
if (providerData.binding) {
|
||||
providerData.binding = undefined;
|
||||
providerData.binding = undefined
|
||||
}
|
||||
};
|
||||
}, [room, activeFileContent]);
|
||||
}
|
||||
}, [room, activeFileContent])
|
||||
|
||||
// Added this effect to clean up when the component unmounts
|
||||
useEffect(() => {
|
||||
@ -416,14 +497,14 @@ export default function CodeEditor({
|
||||
// Clean up all providers when the component unmounts
|
||||
providersMap.current.forEach((data) => {
|
||||
if (data.binding) {
|
||||
data.binding.destroy();
|
||||
data.binding.destroy()
|
||||
}
|
||||
data.provider.disconnect();
|
||||
data.yDoc.destroy();
|
||||
});
|
||||
providersMap.current.clear();
|
||||
};
|
||||
}, []);
|
||||
data.provider.disconnect()
|
||||
data.yDoc.destroy()
|
||||
})
|
||||
providersMap.current.clear()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Connection/disconnection effect
|
||||
useEffect(() => {
|
||||
@ -435,7 +516,7 @@ export default function CodeEditor({
|
||||
|
||||
// Socket event listener effect
|
||||
useEffect(() => {
|
||||
const onConnect = () => { }
|
||||
const onConnect = () => {}
|
||||
|
||||
const onDisconnect = () => {
|
||||
setTerminals([])
|
||||
@ -481,48 +562,55 @@ export default function CodeEditor({
|
||||
socket?.off("disableAccess", onDisableAccess)
|
||||
socket?.off("previewURL", loadPreviewURL)
|
||||
}
|
||||
}, [socket, terminals, setTerminals, setFiles, toast, setDisableAccess, isOwner, loadPreviewURL]);
|
||||
}, [
|
||||
socket,
|
||||
terminals,
|
||||
setTerminals,
|
||||
setFiles,
|
||||
toast,
|
||||
setDisableAccess,
|
||||
isOwner,
|
||||
loadPreviewURL,
|
||||
])
|
||||
|
||||
// Helper functions for tabs:
|
||||
|
||||
// Select file and load content
|
||||
|
||||
// Initialize debounced function once
|
||||
const fileCache = useRef(new Map());
|
||||
const fileCache = useRef(new Map())
|
||||
|
||||
// Debounced function to get file content
|
||||
const debouncedGetFile =
|
||||
(tabId: any, callback: any) => {
|
||||
socket?.emit('getFile', tabId, callback);
|
||||
const debouncedGetFile = (tabId: any, callback: any) => {
|
||||
socket?.emit("getFile", tabId, callback)
|
||||
} // 300ms debounce delay, adjust as needed
|
||||
|
||||
const selectFile = (tab: TTab) => {
|
||||
if (tab.id === activeFileId) return
|
||||
|
||||
if (tab.id === activeFileId) return;
|
||||
setGenerate((prev) => ({ ...prev, show: false }))
|
||||
|
||||
setGenerate((prev) => ({ ...prev, show: false }));
|
||||
|
||||
const exists = tabs.find((t) => t.id === tab.id);
|
||||
const exists = tabs.find((t) => t.id === tab.id)
|
||||
setTabs((prev) => {
|
||||
if (exists) {
|
||||
setActiveFileId(exists.id);
|
||||
return prev;
|
||||
setActiveFileId(exists.id)
|
||||
return prev
|
||||
}
|
||||
return [...prev, tab];
|
||||
});
|
||||
return [...prev, tab]
|
||||
})
|
||||
|
||||
if (fileCache.current.has(tab.id)) {
|
||||
setActiveFileContent(fileCache.current.get(tab.id));
|
||||
setActiveFileContent(fileCache.current.get(tab.id))
|
||||
} else {
|
||||
debouncedGetFile(tab.id, (response: SetStateAction<string>) => {
|
||||
fileCache.current.set(tab.id, response);
|
||||
setActiveFileContent(response);
|
||||
});
|
||||
fileCache.current.set(tab.id, response)
|
||||
setActiveFileContent(response)
|
||||
})
|
||||
}
|
||||
|
||||
setEditorLanguage(processFileType(tab.name));
|
||||
setActiveFileId(tab.id);
|
||||
};
|
||||
setEditorLanguage(processFileType(tab.name))
|
||||
setActiveFileId(tab.id)
|
||||
}
|
||||
|
||||
// Close tab and remove from tabs
|
||||
const closeTab = (id: string) => {
|
||||
@ -538,8 +626,8 @@ export default function CodeEditor({
|
||||
? numTabs === 1
|
||||
? null
|
||||
: index < numTabs - 1
|
||||
? tabs[index + 1].id
|
||||
: tabs[index - 1].id
|
||||
? tabs[index + 1].id
|
||||
: tabs[index - 1].id
|
||||
: activeFileId
|
||||
|
||||
setTabs((prev) => prev.filter((t) => t.id !== id))
|
||||
@ -632,7 +720,7 @@ export default function CodeEditor({
|
||||
<DisableAccessModal
|
||||
message={disableAccess.message}
|
||||
open={disableAccess.isDisabled}
|
||||
setOpen={() => { }}
|
||||
setOpen={() => {}}
|
||||
/>
|
||||
<Loading />
|
||||
</>
|
||||
@ -643,30 +731,74 @@ export default function CodeEditor({
|
||||
{/* Copilot DOM elements */}
|
||||
<PreviewProvider>
|
||||
<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}>
|
||||
{generate.show && ai ? (
|
||||
{generate.show ? (
|
||||
<GenerateInput
|
||||
user={userData}
|
||||
socket={socket!}
|
||||
width={generate.width - 90}
|
||||
data={{
|
||||
fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "",
|
||||
code: editorRef?.getValue() ?? "",
|
||||
code:
|
||||
(isSelected && editorRef?.getSelection()
|
||||
? editorRef
|
||||
?.getModel()
|
||||
?.getValueInRange(editorRef?.getSelection()!)
|
||||
: editorRef?.getValue()) ?? "",
|
||||
line: generate.line,
|
||||
}}
|
||||
editor={{
|
||||
language: editorLanguage,
|
||||
}}
|
||||
onExpand={() => {
|
||||
const line = generate.line
|
||||
|
||||
editorRef?.changeViewZones(function (changeAccessor) {
|
||||
changeAccessor.removeZone(generate.id)
|
||||
|
||||
if (!generateRef.current) return
|
||||
const id = changeAccessor.addZone({
|
||||
afterLineNumber: cursorLine,
|
||||
heightInLines: 12,
|
||||
domNode: generateRef.current,
|
||||
})
|
||||
let id = ""
|
||||
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,
|
||||
heightInLines: 12,
|
||||
|
||||
domNode: generateRef.current,
|
||||
})
|
||||
}
|
||||
setGenerate((prev) => {
|
||||
return { ...prev, id }
|
||||
})
|
||||
@ -680,12 +812,14 @@ export default function CodeEditor({
|
||||
show: !prev.show,
|
||||
}
|
||||
})
|
||||
const file = editorRef?.getValue()
|
||||
|
||||
const lines = file?.split("\n") || []
|
||||
lines.splice(line - 1, 0, code)
|
||||
const updatedFile = lines.join("\n")
|
||||
editorRef?.setValue(updatedFile)
|
||||
const selection = editorRef?.getSelection()
|
||||
const range =
|
||||
isSelected && selection
|
||||
? selection
|
||||
: new monaco.Range(line, 1, line, 1)
|
||||
editorRef?.executeEdits("ai-generation", [
|
||||
{ range, text: code, forceMoveMarkers: true },
|
||||
])
|
||||
}}
|
||||
onClose={() => {
|
||||
setGenerate((prev) => {
|
||||
@ -711,9 +845,6 @@ export default function CodeEditor({
|
||||
setFiles={setFiles}
|
||||
addNew={(name, type) => addNew(name, type, setFiles, sandboxData)}
|
||||
deletingFolderId={deletingFolderId}
|
||||
// AI Copilot Toggle
|
||||
ai={ai}
|
||||
setAi={setAi}
|
||||
/>
|
||||
|
||||
{/* Shadcn resizeable panels: https://ui.shadcn.com/docs/components/resizable */}
|
||||
@ -754,58 +885,58 @@ export default function CodeEditor({
|
||||
</div>
|
||||
</>
|
||||
) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643
|
||||
clerk.loaded ? (
|
||||
<>
|
||||
{provider && userInfo ? (
|
||||
<Cursors yProvider={provider} userInfo={userInfo} />
|
||||
) : null}
|
||||
<Editor
|
||||
height="100%"
|
||||
language={editorLanguage}
|
||||
beforeMount={handleEditorWillMount}
|
||||
onMount={handleEditorMount}
|
||||
onChange={(value) => {
|
||||
if (value === activeFileContent) {
|
||||
setTabs((prev) =>
|
||||
prev.map((tab) =>
|
||||
tab.id === activeFileId
|
||||
? { ...tab, saved: true }
|
||||
: tab
|
||||
)
|
||||
clerk.loaded ? (
|
||||
<>
|
||||
{provider && userInfo ? (
|
||||
<Cursors yProvider={provider} userInfo={userInfo} />
|
||||
) : null}
|
||||
<Editor
|
||||
height="100%"
|
||||
language={editorLanguage}
|
||||
beforeMount={handleEditorWillMount}
|
||||
onMount={handleEditorMount}
|
||||
onChange={(value) => {
|
||||
if (value === activeFileContent) {
|
||||
setTabs((prev) =>
|
||||
prev.map((tab) =>
|
||||
tab.id === activeFileId
|
||||
? { ...tab, saved: true }
|
||||
: tab
|
||||
)
|
||||
} else {
|
||||
setTabs((prev) =>
|
||||
prev.map((tab) =>
|
||||
tab.id === activeFileId
|
||||
? { ...tab, saved: false }
|
||||
: tab
|
||||
)
|
||||
)
|
||||
} else {
|
||||
setTabs((prev) =>
|
||||
prev.map((tab) =>
|
||||
tab.id === activeFileId
|
||||
? { ...tab, saved: false }
|
||||
: tab
|
||||
)
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
tabSize: 2,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
padding: {
|
||||
bottom: 4,
|
||||
top: 4,
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
fixedOverflowWidgets: true,
|
||||
fontFamily: "var(--font-geist-mono)",
|
||||
}}
|
||||
theme="vs-dark"
|
||||
value={activeFileContent}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
|
||||
<Loader2 className="animate-spin w-6 h-6 mr-3" />
|
||||
Waiting for Clerk to load...
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
tabSize: 2,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
padding: {
|
||||
bottom: 4,
|
||||
top: 4,
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
fixedOverflowWidgets: true,
|
||||
fontFamily: "var(--font-geist-mono)",
|
||||
}}
|
||||
theme="vs-dark"
|
||||
value={activeFileContent}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
|
||||
<Loader2 className="animate-spin w-6 h-6 mr-3" />
|
||||
Waiting for Clerk to load...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle />
|
||||
@ -825,7 +956,11 @@ export default function CodeEditor({
|
||||
open={() => {
|
||||
usePreview().previewPanelRef.current?.expand()
|
||||
setIsPreviewCollapsed(false)
|
||||
}} collapsed={isPreviewCollapsed} src={previewURL} ref={previewWindowRef} />
|
||||
}}
|
||||
collapsed={isPreviewCollapsed}
|
||||
src={previewURL}
|
||||
ref={previewWindowRef}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle />
|
||||
<ResizablePanel
|
||||
@ -849,4 +984,3 @@ export default function CodeEditor({
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -32,8 +32,6 @@ export default function Sidebar({
|
||||
socket,
|
||||
setFiles,
|
||||
addNew,
|
||||
ai,
|
||||
setAi,
|
||||
deletingFolderId,
|
||||
}: {
|
||||
sandboxData: Sandbox;
|
||||
@ -50,8 +48,6 @@ export default function Sidebar({
|
||||
socket: Socket;
|
||||
setFiles: (files: (TFile | TFolder)[]) => void;
|
||||
addNew: (name: string, type: "file" | "folder") => void;
|
||||
ai: boolean;
|
||||
setAi: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
deletingFolderId: string;
|
||||
}) {
|
||||
const ref = useRef(null); // drop target
|
||||
@ -186,20 +182,6 @@ export default function Sidebar({
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full space-y-4">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center">
|
||||
<Sparkles
|
||||
className={`h-4 w-4 mr-2 ${
|
||||
ai ? "text-indigo-500" : "text-muted-foreground"
|
||||
}`}
|
||||
/>
|
||||
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>
|
||||
<Switch checked={ai} onCheckedChange={setAi} />
|
||||
</div>
|
||||
{/* <Button className="w-full">
|
||||
<MonitorPlay className="w-4 h-4 mr-2" /> Run
|
||||
</Button> */}
|
||||
|
@ -22,6 +22,7 @@ const buttonVariants = cva(
|
||||
},
|
||||
size: {
|
||||
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",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
|
@ -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="flex items-center">
|
||||
<Sparkles className={`h-4 w-4 mr-2 text-indigo-500`} />
|
||||
AI Usage: {userData.generations}/10
|
||||
AI Usage: {userData.generations}/1000
|
||||
</div>
|
||||
<div className="rounded-full w-full mt-2 h-2 overflow-hidden bg-secondary">
|
||||
<div
|
||||
className="h-full bg-indigo-500 rounded-full"
|
||||
style={{
|
||||
width: `${(userData.generations * 100) / 10}%`,
|
||||
width: `${(userData.generations * 100) / 1000}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
295
frontend/lib/file-extension-to-language.json
Normal file
295
frontend/lib/file-extension-to-language.json
Normal file
@ -0,0 +1,295 @@
|
||||
{
|
||||
"_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"
|
||||
}
|
@ -2,18 +2,19 @@ import { type ClassValue, clsx } from "clsx"
|
||||
// import { toast } from "sonner"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { Sandbox, TFile, TFolder } from "./types"
|
||||
import fileExtToLang from "./file-extension-to-language.json"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export function processFileType(file: string) {
|
||||
const ending = file.split(".").pop()
|
||||
const extension = 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"
|
||||
}
|
||||
|
||||
@ -62,12 +63,15 @@ export function addNew(
|
||||
}
|
||||
}
|
||||
|
||||
export function debounce<T extends (...args: any[]) => void>(func: T, wait: number): T {
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
export function debounce<T extends (...args: any[]) => void>(
|
||||
func: T,
|
||||
wait: number
|
||||
): T {
|
||||
let timeout: NodeJS.Timeout | null = null
|
||||
return function (...args: Parameters<T>) {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(() => func(...args), wait);
|
||||
} as T;
|
||||
}
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
timeout = setTimeout(() => func(...args), wait)
|
||||
} as T
|
||||
}
|
||||
|
Reference in New Issue
Block a user