Compare commits
23 Commits
refactor
...
sync-conta
Author | SHA1 | Date | |
---|---|---|---|
023b3bdc5e | |||
13be78dee8 | |||
7a00d24ab9 | |||
69b1287349 | |||
c94678c430 | |||
585dcb469e | |||
2f88ff6d58 | |||
0509716f34 | |||
06118e98e9 | |||
4ebd6dea96 | |||
8921cd83bb | |||
45097e0f20 | |||
62e6d64a52 | |||
0c6b2b0dfb | |||
31d74ddc2d | |||
62311faf51 | |||
208d17879f | |||
0067dc8c0c | |||
4fe749daf2 | |||
b01934bd20 | |||
a1990a189c | |||
bf79893dfa | |||
47324f15bf |
216
backend/ai/package-lock.json
generated
216
backend/ai/package-lock.json
generated
@ -7,6 +7,9 @@
|
|||||||
"": {
|
"": {
|
||||||
"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",
|
||||||
@ -15,6 +18,28 @@
|
|||||||
"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",
|
||||||
@ -861,11 +886,19 @@
|
|||||||
"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",
|
||||||
@ -944,6 +977,17 @@
|
|||||||
"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",
|
||||||
@ -965,6 +1009,17 @@
|
|||||||
"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",
|
||||||
@ -1008,6 +1063,11 @@
|
|||||||
"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",
|
||||||
@ -1141,6 +1201,17 @@
|
|||||||
"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",
|
||||||
@ -1214,6 +1285,14 @@
|
|||||||
"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",
|
||||||
@ -1287,6 +1366,14 @@
|
|||||||
"@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",
|
||||||
@ -1334,6 +1421,36 @@
|
|||||||
"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",
|
||||||
@ -1452,6 +1569,14 @@
|
|||||||
"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",
|
||||||
@ -1610,6 +1735,25 @@
|
|||||||
"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",
|
||||||
@ -1675,8 +1819,7 @@
|
|||||||
"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",
|
||||||
@ -1705,6 +1848,43 @@
|
|||||||
"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",
|
||||||
@ -2222,6 +2402,11 @@
|
|||||||
"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",
|
||||||
@ -2292,8 +2477,7 @@
|
|||||||
"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",
|
||||||
@ -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": {
|
"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",
|
||||||
|
@ -15,5 +15,8 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,43 +1,77 @@
|
|||||||
|
import { Anthropic } from "@anthropic-ai/sdk";
|
||||||
|
|
||||||
export interface Env {
|
export interface Env {
|
||||||
AI: any
|
ANTHROPIC_API_KEY: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async fetch(request, env): Promise<Response> {
|
async fetch(request: Request, env: Env): Promise<Response> {
|
||||||
if (request.method !== "GET") {
|
if (request.method !== "GET") {
|
||||||
return new Response("Method Not Allowed", { status: 405 })
|
return new Response("Method Not Allowed", { status: 405 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL(request.url)
|
const url = new URL(request.url);
|
||||||
const fileName = url.searchParams.get("fileName")
|
// const fileName = url.searchParams.get("fileName");
|
||||||
const instructions = url.searchParams.get("instructions")
|
// const line = url.searchParams.get("line");
|
||||||
const line = url.searchParams.get("line")
|
const instructions = url.searchParams.get("instructions");
|
||||||
const code = url.searchParams.get("code")
|
const code = url.searchParams.get("code");
|
||||||
|
|
||||||
const response = await env.AI.run("@cf/meta/llama-3-8b-instruct", {
|
const prompt = `
|
||||||
messages: [
|
Make the following changes to the code below:
|
||||||
{
|
- ${instructions}
|
||||||
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}`,
|
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.
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
return new Response(JSON.stringify(response))
|
\`\`\`
|
||||||
|
${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 });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
} satisfies ExportedHandler<Env>
|
};
|
||||||
|
106
backend/server/package-lock.json
generated
106
backend/server/package-lock.json
generated
@ -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.1",
|
"e2b": "^0.16.2-beta.47",
|
||||||
"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,6 +41,28 @@
|
|||||||
"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",
|
||||||
@ -443,6 +465,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@ -564,6 +587,11 @@
|
|||||||
"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",
|
||||||
@ -737,23 +765,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/e2b": {
|
"node_modules/e2b": {
|
||||||
"version": "0.16.2",
|
"version": "0.16.2-beta.47",
|
||||||
"resolved": "https://registry.npmjs.org/e2b/-/e2b-0.16.2.tgz",
|
"resolved": "https://registry.npmjs.org/e2b/-/e2b-0.16.2-beta.47.tgz",
|
||||||
"integrity": "sha512-xKmVK4ipgVQPJ/uyyrfH9LnaawERRWt8U2UZhdhGfzdL/QU/OpBjuhoIbFCv1Uy6qXV4nIiJ6Nw4MBC4HmXf1g==",
|
"integrity": "sha512-tMPDYLMD+8+JyLPrsWft3NHBhK5YKOFOXzKMwpOKR5KvXOkd1silkArDwplmBUzN/eG/uRzWdtHZs9mHUQ5b9g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"isomorphic-ws": "^5.0.0",
|
"@bufbuild/protobuf": "^1.10.0",
|
||||||
"normalize-path": "^3.0.0",
|
"@connectrpc/connect": "^1.4.0",
|
||||||
"openapi-typescript-fetch": "^1.1.3",
|
"@connectrpc/connect-web": "^1.4.0",
|
||||||
"path-browserify": "^1.0.1",
|
"compare-versions": "^6.1.0",
|
||||||
"platform": "^1.3.6",
|
"openapi-fetch": "^0.9.7",
|
||||||
"ws": "^8.15.1"
|
"platform": "^1.3.6"
|
||||||
},
|
},
|
||||||
"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": {
|
||||||
@ -1195,14 +1219,6 @@
|
|||||||
"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",
|
||||||
@ -1301,6 +1317,7 @@
|
|||||||
"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",
|
||||||
@ -1383,6 +1400,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@ -1417,15 +1435,19 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/openapi-typescript-fetch": {
|
"node_modules/openapi-fetch": {
|
||||||
"version": "1.1.3",
|
"version": "0.9.8",
|
||||||
"resolved": "https://registry.npmjs.org/openapi-typescript-fetch/-/openapi-typescript-fetch-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.9.8.tgz",
|
||||||
"integrity": "sha512-smLZPck4OkKMNExcw8jMgrMOGgVGx2N/s6DbKL2ftNl77g5HfoGpZGFy79RBzU/EkaO0OZpwBnslfdBfh7ZcWg==",
|
"integrity": "sha512-zM6elH0EZStD/gSiNlcPrzXcVQ/pZo3BDvC6CDwRDUt1dDzxlshpmQnpD6cZaJ39THaSmwVCxxRrPKNM1hHrDg==",
|
||||||
"engines": {
|
"dependencies": {
|
||||||
"node": ">= 12.0.0",
|
"openapi-typescript-helpers": "^0.0.8"
|
||||||
"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",
|
||||||
@ -1434,11 +1456,6 @@
|
|||||||
"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",
|
||||||
@ -2053,6 +2070,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@ -2098,26 +2116,6 @@
|
|||||||
"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",
|
||||||
|
@ -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.1",
|
"e2b": "^0.16.2-beta.47",
|
||||||
"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",
|
||||||
|
68
backend/server/src/Terminal.ts
Normal file
68
backend/server/src/Terminal.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
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));
|
||||||
|
await this.pty.wait();
|
||||||
|
} 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();
|
@ -6,10 +6,15 @@ 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 from "fs";
|
import fs, { readFile } from "fs";
|
||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { User } from "./types";
|
import {
|
||||||
|
TFile,
|
||||||
|
TFileData,
|
||||||
|
TFolder,
|
||||||
|
User
|
||||||
|
} from "./types";
|
||||||
import {
|
import {
|
||||||
createFile,
|
createFile,
|
||||||
deleteFile,
|
deleteFile,
|
||||||
@ -20,7 +25,11 @@ 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 } from "e2b";
|
||||||
|
|
||||||
|
import { Terminal } from "./Terminal"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MAX_BODY_SIZE,
|
MAX_BODY_SIZE,
|
||||||
createFileRL,
|
createFileRL,
|
||||||
@ -30,6 +39,18 @@ 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
|
||||||
|
});
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const app: Express = express();
|
const app: Express = express();
|
||||||
@ -51,14 +72,14 @@ const terminals: Record<string, Terminal> = {};
|
|||||||
|
|
||||||
const dirName = "/home/user";
|
const dirName = "/home/user";
|
||||||
|
|
||||||
const moveFile = async (
|
const moveFile = async (filesystem: Filesystem, filePath: string, newFilePath: string) => {
|
||||||
filesystem: FilesystemManager,
|
try {
|
||||||
filePath: string,
|
const fileContents = await filesystem.read(filePath);
|
||||||
newFilePath: string
|
await filesystem.write(newFilePath, fileContents);
|
||||||
) => {
|
|
||||||
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) => {
|
||||||
@ -153,11 +174,12 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await lockManager.acquireLock(data.sandboxId, async () => {
|
const createdContainer = await lockManager.acquireLock(data.sandboxId, async () => {
|
||||||
try {
|
try {
|
||||||
if (!containers[data.sandboxId]) {
|
if (!containers[data.sandboxId]) {
|
||||||
containers[data.sandboxId] = await Sandbox.create();
|
containers[data.sandboxId] = await Sandbox.create({ timeoutMs: 1200000 });
|
||||||
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);
|
||||||
@ -165,22 +187,170 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sandboxFiles = await getSandboxFiles(data.sandboxId);
|
||||||
|
const projectDirectory = path.join(dirName, "projects", data.sandboxId);
|
||||||
|
const containerFiles = containers[data.sandboxId].files;
|
||||||
|
|
||||||
// Change the owner of the project directory to user
|
// Change the owner of the project directory to user
|
||||||
const fixPermissions = async () => {
|
const fixPermissions = async (projectDirectory: string) => {
|
||||||
await containers[data.sandboxId].process.startAndWait(
|
try {
|
||||||
`sudo chown -R user "${path.join(dirName, "projects", data.sandboxId)}"`
|
await containers[data.sandboxId].commands.run(
|
||||||
|
`sudo chown -R user "${projectDirectory}"`
|
||||||
);
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log("Failed to fix permissions: " + e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const sandboxFiles = await getSandboxFiles(data.sandboxId);
|
// Check if the given path is a directory
|
||||||
sandboxFiles.fileData.forEach(async (file) => {
|
const isDirectory = async (projectDirectory: string): Promise<boolean> => {
|
||||||
const filePath = path.join(dirName, file.id);
|
try {
|
||||||
await containers[data.sandboxId].filesystem.makeDir(
|
const result = await containers[data.sandboxId].commands.run(
|
||||||
path.dirname(filePath)
|
`[ -d "${projectDirectory}" ] && echo "true" || echo "false"`
|
||||||
);
|
);
|
||||||
await containers[data.sandboxId].filesystem.write(filePath, file.data);
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
fixPermissions();
|
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.emit("loaded", sandboxFiles.files);
|
||||||
|
|
||||||
@ -231,11 +401,11 @@ io.on("connection", async (socket) => {
|
|||||||
if (!file) return;
|
if (!file) return;
|
||||||
file.data = body;
|
file.data = body;
|
||||||
|
|
||||||
await containers[data.sandboxId].filesystem.write(
|
await containers[data.sandboxId].files.write(
|
||||||
path.join(dirName, file.id),
|
path.join(dirName, file.id),
|
||||||
body
|
body
|
||||||
);
|
);
|
||||||
fixPermissions();
|
fixPermissions(projectDirectory);
|
||||||
} 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}`);
|
||||||
@ -253,11 +423,11 @@ io.on("connection", async (socket) => {
|
|||||||
const newFileId = folderId + "/" + parts.pop();
|
const newFileId = folderId + "/" + parts.pop();
|
||||||
|
|
||||||
await moveFile(
|
await moveFile(
|
||||||
containers[data.sandboxId].filesystem,
|
containers[data.sandboxId].files,
|
||||||
path.join(dirName, fileId),
|
path.join(dirName, fileId),
|
||||||
path.join(dirName, newFileId)
|
path.join(dirName, newFileId)
|
||||||
);
|
);
|
||||||
fixPermissions();
|
fixPermissions(projectDirectory);
|
||||||
|
|
||||||
file.id = newFileId;
|
file.id = newFileId;
|
||||||
|
|
||||||
@ -346,11 +516,11 @@ io.on("connection", async (socket) => {
|
|||||||
|
|
||||||
const id = `projects/${data.sandboxId}/${name}`;
|
const id = `projects/${data.sandboxId}/${name}`;
|
||||||
|
|
||||||
await containers[data.sandboxId].filesystem.write(
|
await containers[data.sandboxId].files.write(
|
||||||
path.join(dirName, id),
|
path.join(dirName, id),
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
fixPermissions();
|
fixPermissions(projectDirectory);
|
||||||
|
|
||||||
sandboxFiles.files.push({
|
sandboxFiles.files.push({
|
||||||
id,
|
id,
|
||||||
@ -383,7 +553,7 @@ io.on("connection", async (socket) => {
|
|||||||
|
|
||||||
const id = `projects/${data.sandboxId}/${name}`;
|
const id = `projects/${data.sandboxId}/${name}`;
|
||||||
|
|
||||||
await containers[data.sandboxId].filesystem.makeDir(
|
await containers[data.sandboxId].files.makeDir(
|
||||||
path.join(dirName, id)
|
path.join(dirName, id)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -412,11 +582,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].filesystem,
|
containers[data.sandboxId].files,
|
||||||
path.join(dirName, fileId),
|
path.join(dirName, fileId),
|
||||||
path.join(dirName, newFileId)
|
path.join(dirName, newFileId)
|
||||||
);
|
);
|
||||||
fixPermissions();
|
fixPermissions(projectDirectory);
|
||||||
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);
|
||||||
@ -435,7 +605,7 @@ 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].filesystem.remove(
|
await containers[data.sandboxId].files.remove(
|
||||||
path.join(dirName, fileId)
|
path.join(dirName, fileId)
|
||||||
);
|
);
|
||||||
sandboxFiles.fileData = sandboxFiles.fileData.filter(
|
sandboxFiles.fileData = sandboxFiles.fileData.filter(
|
||||||
@ -462,7 +632,7 @@ io.on("connection", async (socket) => {
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
files.map(async (file) => {
|
files.map(async (file) => {
|
||||||
await containers[data.sandboxId].filesystem.remove(
|
await containers[data.sandboxId].files.remove(
|
||||||
path.join(dirName, file)
|
path.join(dirName, file)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -491,35 +661,36 @@ io.on("connection", async (socket) => {
|
|||||||
|
|
||||||
await lockManager.acquireLock(data.sandboxId, async () => {
|
await lockManager.acquireLock(data.sandboxId, async () => {
|
||||||
try {
|
try {
|
||||||
terminals[id] = await containers[data.sandboxId].terminal.start({
|
terminals[id] = new Terminal(containers[data.sandboxId])
|
||||||
onData: (responseData: string) => {
|
await terminals[id].init({
|
||||||
io.emit("terminalResponse", { id, data: responseData });
|
onData: (responseString: string) => {
|
||||||
|
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(responseData) ?? "");
|
const port = parseInt(extractPortNumber(responseString) ?? "");
|
||||||
if (port) {
|
if (port) {
|
||||||
io.emit(
|
io.emit(
|
||||||
"previewURL",
|
"previewURL",
|
||||||
"https://" + containers[data.sandboxId].getHostname(port)
|
"https://" + containers[data.sandboxId].getHost(port)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
size: { cols: 80, rows: 20 },
|
cols: 80,
|
||||||
onExit: () => console.log("Terminal exited", id),
|
rows: 20,
|
||||||
|
//onExit: () => console.log("Terminal exited", id),
|
||||||
});
|
});
|
||||||
await terminals[id].sendData(
|
await terminals[id].sendData(
|
||||||
`cd "${path.join(dirName, "projects", data.sandboxId)}"\r`
|
`cd "${path.join(dirName, "projects", data.sandboxId)}"\rexport PS1='user> '\rclear\r`
|
||||||
);
|
);
|
||||||
await terminals[id].sendData("export PS1='user> '\rclear\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);
|
||||||
@ -548,7 +719,7 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
socket.on("terminalData", (id: string, data: string) => {
|
socket.on("terminalData", async (id: string, data: string) => {
|
||||||
try {
|
try {
|
||||||
if (!terminals[id]) {
|
if (!terminals[id]) {
|
||||||
return;
|
return;
|
||||||
@ -567,7 +738,7 @@ io.on("connection", async (socket) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await terminals[id].kill();
|
await terminals[id].close();
|
||||||
delete terminals[id];
|
delete terminals[id];
|
||||||
|
|
||||||
callback();
|
callback();
|
||||||
@ -603,7 +774,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=${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: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@ -636,7 +807,7 @@ io.on("connection", async (socket) => {
|
|||||||
if (data.isOwner && connections[data.sandboxId] <= 0) {
|
if (data.isOwner && connections[data.sandboxId] <= 0) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Object.entries(terminals).map(async ([key, terminal]) => {
|
Object.entries(terminals).map(async ([key, terminal]) => {
|
||||||
await terminal.kill();
|
await terminal.close();
|
||||||
delete terminals[key];
|
delete terminals[key];
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -644,7 +815,7 @@ io.on("connection", async (socket) => {
|
|||||||
await lockManager.acquireLock(data.sandboxId, async () => {
|
await lockManager.acquireLock(data.sandboxId, async () => {
|
||||||
try {
|
try {
|
||||||
if (containers[data.sandboxId]) {
|
if (containers[data.sandboxId]) {
|
||||||
await containers[data.sandboxId].close();
|
await containers[data.sandboxId].kill();
|
||||||
delete containers[data.sandboxId];
|
delete containers[data.sandboxId];
|
||||||
console.log("Closed container", data.sandboxId);
|
console.log("Closed container", data.sandboxId);
|
||||||
}
|
}
|
||||||
|
32
backend/storage/package-lock.json
generated
32
backend/storage/package-lock.json
generated
@ -8,6 +8,7 @@
|
|||||||
"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": {
|
||||||
@ -894,6 +895,21 @@
|
|||||||
"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",
|
||||||
@ -1766,12 +1782,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/p-limit": {
|
"node_modules/p-limit": {
|
||||||
"version": "5.0.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.1.0.tgz",
|
||||||
"integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==",
|
"integrity": "sha512-H0jc0q1vOzlEk0TqAKXKZxdl7kX3OFUzCnNVUnq5Pc3DGo0kpeaMuPqxQn235HibwBEb0/pm9dgKTjXy66fBkg==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"yocto-queue": "^1.0.0"
|
"yocto-queue": "^1.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@ -2970,10 +2985,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/yocto-queue": {
|
"node_modules/yocto-queue": {
|
||||||
"version": "1.0.0",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz",
|
||||||
"integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==",
|
"integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.20"
|
"node": ">=12.20"
|
||||||
},
|
},
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
"wrangler": "^3.0.0"
|
"wrangler": "^3.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"p-limit": "^6.1.0",
|
||||||
"zod": "^3.23.4"
|
"zod": "^3.23.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,17 +146,18 @@ export default {
|
|||||||
|
|
||||||
console.log(`Copying template: ${type}`);
|
console.log(`Copying template: ${type}`);
|
||||||
|
|
||||||
const templateDirectory = `templates/${type}`;
|
|
||||||
|
|
||||||
// List all objects under the directory
|
// 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
|
// Copy each object to the new directory with a 5 concurrency limit
|
||||||
for (const { key } of objects) {
|
const limit = pLimit(5);
|
||||||
const destinationKey = key.replace(templateDirectory, `projects/${sandboxId}`);
|
await Promise.all(objects.map(({ key }) =>
|
||||||
const fileBody = await env.R2.get(key).then(res => res?.body ?? "");
|
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);
|
await env.R2.put(destinationKey, fileBody);
|
||||||
}
|
})
|
||||||
|
));
|
||||||
|
|
||||||
return success
|
return success
|
||||||
} else {
|
} else {
|
||||||
|
@ -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>
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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 />
|
||||||
|
@ -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 (
|
||||||
<>
|
<>
|
||||||
|
@ -93,7 +93,7 @@ export default function NewProjectModal({
|
|||||||
open: boolean
|
open: boolean
|
||||||
setOpen: (open: boolean) => void
|
setOpen: (open: boolean) => void
|
||||||
}) {
|
}) {
|
||||||
const [selected, setSelected] = useState("react")
|
const [selected, setSelected] = useState("reactjs")
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useCallback, 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 >= 10) {
|
if (user.generations >= 1000) {
|
||||||
toast.error("You reached the maximum # of generations.")
|
toast.error("You reached the maximum # of generations.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -84,6 +84,13 @@ export default function GenerateInput({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
const handleGenerateForm = useCallback(
|
||||||
|
(e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
handleGenerate({ regenerate: false })
|
||||||
|
},
|
||||||
|
[input, currentPrompt]
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (code) {
|
if (code) {
|
||||||
@ -93,9 +100,23 @@ 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">
|
||||||
<div className="flex items-center font-sans space-x-2">
|
<form
|
||||||
|
onSubmit={handleGenerateForm}
|
||||||
|
className="flex items-center font-sans space-x-2"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
style={{
|
style={{
|
||||||
@ -109,8 +130,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 ? (
|
||||||
<>
|
<>
|
||||||
@ -126,13 +147,14 @@ 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>
|
||||||
</div>
|
</form>
|
||||||
{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">
|
||||||
|
@ -1,48 +1,40 @@
|
|||||||
"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 { Sandbox, User, TFile, TFolder, TTab } from "@/lib/types"
|
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
|
||||||
|
import { toast } from "sonner"
|
||||||
// Authentication
|
|
||||||
import { useClerk } from "@clerk/nextjs"
|
import { useClerk } from "@clerk/nextjs"
|
||||||
|
import { AnimatePresence, motion } from "framer-motion"
|
||||||
|
|
||||||
// Liveblocks
|
|
||||||
import * as Y from "yjs"
|
import * as Y from "yjs"
|
||||||
import LiveblocksProvider from "@liveblocks/yjs"
|
import LiveblocksProvider from "@liveblocks/yjs"
|
||||||
import { MonacoBinding } from "y-monaco"
|
import { MonacoBinding } from "y-monaco"
|
||||||
import { Awareness } from "y-protocols/awareness"
|
import { Awareness } from "y-protocols/awareness"
|
||||||
import { TypedLiveblocksProvider, useRoom, useSelf } from "@/liveblocks.config"
|
import { TypedLiveblocksProvider, useRoom, useSelf } from "@/liveblocks.config"
|
||||||
|
|
||||||
// Icons
|
|
||||||
import { FileJson, Loader2, TerminalSquare } from "lucide-react"
|
|
||||||
|
|
||||||
// Contexts
|
|
||||||
import { PreviewProvider, usePreview } from '@/context/PreviewContext';
|
|
||||||
import { useSocket } from "@/context/SocketContext"
|
|
||||||
|
|
||||||
// External Components
|
|
||||||
import monaco from "monaco-editor"
|
|
||||||
import { Terminal } from "@xterm/xterm"
|
|
||||||
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
|
|
||||||
import { toast } from "sonner"
|
|
||||||
import {
|
import {
|
||||||
ResizableHandle,
|
ResizableHandle,
|
||||||
ResizablePanel,
|
ResizablePanel,
|
||||||
ResizablePanelGroup,
|
ResizablePanelGroup,
|
||||||
} from "@/components/ui/resizable"
|
} from "@/components/ui/resizable"
|
||||||
import { ImperativePanelHandle } from "react-resizable-panels"
|
import { FileJson, Loader2, Sparkles, TerminalSquare } from "lucide-react"
|
||||||
|
|
||||||
// Editor Components
|
|
||||||
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"
|
||||||
|
import { Sandbox, User, TFile, TFolder, TTab } from "@/lib/types"
|
||||||
import { addNew, processFileType, validateName, debounce } from "@/lib/utils"
|
import { addNew, processFileType, validateName, debounce } from "@/lib/utils"
|
||||||
import { Cursors } from "./live/cursors"
|
import { Cursors } from "./live/cursors"
|
||||||
|
import { Terminal } from "@xterm/xterm"
|
||||||
import DisableAccessModal from "./live/disableModal"
|
import DisableAccessModal from "./live/disableModal"
|
||||||
import Loading from "./loading"
|
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 { PreviewProvider, usePreview } from "@/context/PreviewContext"
|
||||||
|
import { useSocket } from "@/context/SocketContext"
|
||||||
|
import { Button } from "../ui/button"
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
export default function CodeEditor({
|
export default function CodeEditor({
|
||||||
userData,
|
userData,
|
||||||
@ -51,18 +43,22 @@ export default function CodeEditor({
|
|||||||
userData: User
|
userData: User
|
||||||
sandboxData: Sandbox
|
sandboxData: Sandbox
|
||||||
}) {
|
}) {
|
||||||
|
//SocketContext functions and effects
|
||||||
|
const { socket, setUserAndSandboxId } = useSocket()
|
||||||
|
|
||||||
// Socket to the backend server
|
|
||||||
const { socket, setUserAndSandboxId } = useSocket();
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Pass the user and sandbox ID to the socket, causing the socket to be created and to connect.
|
// Ensure userData.id and sandboxData.id are available before attempting to connect
|
||||||
setUserAndSandboxId(userData.id, sandboxData.id);
|
if (userData.id && sandboxData.id) {
|
||||||
}, []);
|
// Check if the socket is not initialized or not connected
|
||||||
|
if (!socket || (socket && !socket.connected)) {
|
||||||
|
// Initialize socket connection
|
||||||
|
setUserAndSandboxId(userData.id, sandboxData.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [socket, userData.id, sandboxData.id, setUserAndSandboxId])
|
||||||
|
|
||||||
// Preview panel state
|
//Preview Button state
|
||||||
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
|
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
|
||||||
|
|
||||||
// When the owner closes the project, isDisabled gets set for the other users.
|
|
||||||
const [disableAccess, setDisableAccess] = useState({
|
const [disableAccess, setDisableAccess] = useState({
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
message: "",
|
message: "",
|
||||||
@ -82,7 +78,6 @@ 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
|
||||||
@ -95,7 +90,8 @@ 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<
|
||||||
{
|
{
|
||||||
@ -105,13 +101,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
|
||||||
@ -124,23 +120,29 @@ 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 = useRef<ImperativePanelHandle>(null)
|
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([
|
||||||
@ -157,6 +159,13 @@ export default function CodeEditor({
|
|||||||
monacoRef.current = monaco
|
monacoRef.current = monaco
|
||||||
|
|
||||||
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)
|
||||||
@ -210,21 +219,51 @@ export default function CodeEditor({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const handleAiEdit = React.useCallback(() => {
|
||||||
// Generate widget effect
|
if (!editorRef) return
|
||||||
useEffect(() => {
|
const selection = editorRef.getSelection()
|
||||||
if (!ai) {
|
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) => {
|
setGenerate((prev) => {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
show: false,
|
show: true,
|
||||||
|
pref: [pref],
|
||||||
|
id,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return
|
}, [editorRef])
|
||||||
}
|
// Generate widget effect
|
||||||
|
useEffect(() => {
|
||||||
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,
|
||||||
@ -233,6 +272,10 @@ 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
|
||||||
@ -292,6 +335,41 @@ 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(() => {
|
||||||
@ -299,8 +377,6 @@ export default function CodeEditor({
|
|||||||
decorations.instance?.clear()
|
decorations.instance?.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ai) return
|
|
||||||
|
|
||||||
const model = editorRef?.getModel()
|
const model = editorRef?.getModel()
|
||||||
const line = model?.getLineContent(cursorLine)
|
const line = model?.getLineContent(cursorLine)
|
||||||
|
|
||||||
@ -331,27 +407,27 @@ export default function CodeEditor({
|
|||||||
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...${value}`);
|
console.log(`Saving file...${value}`)
|
||||||
socket?.emit("saveFile", activeFileId, value);
|
socket?.emit("saveFile", activeFileId, value)
|
||||||
}, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000),
|
}, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000),
|
||||||
[socket]
|
[socket]
|
||||||
);
|
)
|
||||||
|
|
||||||
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(editorRef?.getValue(), activeFileId);
|
debouncedSaveData(editorRef?.getValue(), activeFileId)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
document.addEventListener("keydown", down);
|
document.addEventListener("keydown", down)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("keydown", down);
|
document.removeEventListener("keydown", down)
|
||||||
};
|
}
|
||||||
}, [activeFileId, tabs, debouncedSaveData]);
|
}, [activeFileId, tabs, debouncedSaveData])
|
||||||
|
|
||||||
// Liveblocks live collaboration setup effect
|
// Liveblocks live collaboration setup effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -360,13 +436,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) => {
|
||||||
@ -387,12 +463,11 @@ 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(
|
||||||
@ -400,21 +475,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(() => {
|
||||||
@ -422,14 +497,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(() => {
|
||||||
@ -441,7 +516,7 @@ export default function CodeEditor({
|
|||||||
|
|
||||||
// Socket event listener effect
|
// Socket event listener effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onConnect = () => { }
|
const onConnect = () => {}
|
||||||
|
|
||||||
const onDisconnect = () => {
|
const onDisconnect = () => {
|
||||||
setTerminals([])
|
setTerminals([])
|
||||||
@ -487,48 +562,55 @@ 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 =
|
const debouncedGetFile = (tabId: any, callback: any) => {
|
||||||
(tabId: any, callback: any) => {
|
socket?.emit("getFile", tabId, callback)
|
||||||
socket?.emit('getFile', tabId, callback);
|
|
||||||
} // 300ms debounce delay, adjust as needed
|
} // 300ms debounce delay, adjust as needed
|
||||||
|
|
||||||
const selectFile = (tab: TTab) => {
|
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) => {
|
setTabs((prev) => {
|
||||||
if (exists) {
|
if (exists) {
|
||||||
setActiveFileId(exists.id);
|
setActiveFileId(exists.id)
|
||||||
return prev;
|
return prev
|
||||||
}
|
}
|
||||||
return [...prev, tab];
|
return [...prev, tab]
|
||||||
});
|
})
|
||||||
|
|
||||||
if (fileCache.current.has(tab.id)) {
|
if (fileCache.current.has(tab.id)) {
|
||||||
setActiveFileContent(fileCache.current.get(tab.id));
|
setActiveFileContent(fileCache.current.get(tab.id))
|
||||||
} else {
|
} else {
|
||||||
debouncedGetFile(tab.id, (response: SetStateAction<string>) => {
|
debouncedGetFile(tab.id, (response: SetStateAction<string>) => {
|
||||||
fileCache.current.set(tab.id, response);
|
fileCache.current.set(tab.id, response)
|
||||||
setActiveFileContent(response);
|
setActiveFileContent(response)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
setEditorLanguage(processFileType(tab.name));
|
setEditorLanguage(processFileType(tab.name))
|
||||||
setActiveFileId(tab.id);
|
setActiveFileId(tab.id)
|
||||||
};
|
}
|
||||||
|
|
||||||
// Close tab and remove from tabs
|
// Close tab and remove from tabs
|
||||||
const closeTab = (id: string) => {
|
const closeTab = (id: string) => {
|
||||||
@ -638,7 +720,7 @@ export default function CodeEditor({
|
|||||||
<DisableAccessModal
|
<DisableAccessModal
|
||||||
message={disableAccess.message}
|
message={disableAccess.message}
|
||||||
open={disableAccess.isDisabled}
|
open={disableAccess.isDisabled}
|
||||||
setOpen={() => { }}
|
setOpen={() => {}}
|
||||||
/>
|
/>
|
||||||
<Loading />
|
<Loading />
|
||||||
</>
|
</>
|
||||||
@ -649,30 +731,74 @@ 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 && ai ? (
|
{generate.show ? (
|
||||||
<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: editorRef?.getValue() ?? "",
|
code:
|
||||||
|
(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
|
||||||
const id = changeAccessor.addZone({
|
let id = ""
|
||||||
afterLineNumber: cursorLine,
|
if (isSelected) {
|
||||||
heightInLines: 12,
|
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,
|
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) => {
|
setGenerate((prev) => {
|
||||||
return { ...prev, id }
|
return { ...prev, id }
|
||||||
})
|
})
|
||||||
@ -686,12 +812,14 @@ export default function CodeEditor({
|
|||||||
show: !prev.show,
|
show: !prev.show,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const file = editorRef?.getValue()
|
const selection = editorRef?.getSelection()
|
||||||
|
const range =
|
||||||
const lines = file?.split("\n") || []
|
isSelected && selection
|
||||||
lines.splice(line - 1, 0, code)
|
? selection
|
||||||
const updatedFile = lines.join("\n")
|
: new monaco.Range(line, 1, line, 1)
|
||||||
editorRef?.setValue(updatedFile)
|
editorRef?.executeEdits("ai-generation", [
|
||||||
|
{ range, text: code, forceMoveMarkers: true },
|
||||||
|
])
|
||||||
}}
|
}}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setGenerate((prev) => {
|
setGenerate((prev) => {
|
||||||
@ -717,9 +845,6 @@ 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}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Shadcn resizeable panels: https://ui.shadcn.com/docs/components/resizable */}
|
{/* Shadcn resizeable panels: https://ui.shadcn.com/docs/components/resizable */}
|
||||||
@ -831,7 +956,11 @@ export default function CodeEditor({
|
|||||||
open={() => {
|
open={() => {
|
||||||
usePreview().previewPanelRef.current?.expand()
|
usePreview().previewPanelRef.current?.expand()
|
||||||
setIsPreviewCollapsed(false)
|
setIsPreviewCollapsed(false)
|
||||||
}} collapsed={isPreviewCollapsed} src={previewURL} ref={previewWindowRef} />
|
}}
|
||||||
|
collapsed={isPreviewCollapsed}
|
||||||
|
src={previewURL}
|
||||||
|
ref={previewWindowRef}
|
||||||
|
/>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle />
|
<ResizableHandle />
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
@ -855,4 +984,3 @@ export default function CodeEditor({
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ export default function Navbar({
|
|||||||
const [isShareOpen, setIsShareOpen] = useState(false);
|
const [isShareOpen, setIsShareOpen] = useState(false);
|
||||||
const [isRunning, setIsRunning] = useState(false);
|
const [isRunning, setIsRunning] = useState(false);
|
||||||
|
|
||||||
const isOwner = sandboxData.userId === userData.id;
|
const isOwner = sandboxData.userId === userData.id;;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -32,8 +32,6 @@ export default function Sidebar({
|
|||||||
socket,
|
socket,
|
||||||
setFiles,
|
setFiles,
|
||||||
addNew,
|
addNew,
|
||||||
ai,
|
|
||||||
setAi,
|
|
||||||
deletingFolderId,
|
deletingFolderId,
|
||||||
}: {
|
}: {
|
||||||
sandboxData: Sandbox;
|
sandboxData: Sandbox;
|
||||||
@ -50,8 +48,6 @@ 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
|
||||||
@ -186,20 +182,6 @@ export default function Sidebar({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full space-y-4">
|
<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">
|
{/* <Button className="w-full">
|
||||||
<MonitorPlay className="w-4 h-4 mr-2" /> Run
|
<MonitorPlay className="w-4 h-4 mr-2" /> Run
|
||||||
</Button> */}
|
</Button> */}
|
||||||
|
@ -22,6 +22,7 @@ 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",
|
||||||
|
@ -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}/10
|
AI Usage: {userData.generations}/1000
|
||||||
</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) / 10}%`,
|
width: `${(userData.generations * 100) / 1000}%`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 { 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 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"
|
return "plaintext"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,12 +63,15 @@ export function addNew(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function debounce<T extends (...args: any[]) => void>(func: T, wait: number): T {
|
export function debounce<T extends (...args: any[]) => void>(
|
||||||
let timeout: NodeJS.Timeout | null = null;
|
func: T,
|
||||||
|
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
|
||||||
}
|
}
|
Reference in New Issue
Block a user