Compare commits

...

53 Commits

Author SHA1 Message Date
2c058b259a chore: remove unnecessary code 2024-10-02 18:29:09 -04:00
5817b2ea48 fix: filecontent update while switching tabs, empty file crash 2024-09-28 19:25:03 -04:00
6845e1fef9 fix: close the terminal opened with run button 2024-09-22 00:23:12 -04:00
f38919d6cf chore: change path.join to path.posix.join 2024-09-16 16:28:58 -04:00
7aaa920815 Merge branch 'refs/heads/main' into production 2024-09-16 08:57:54 -07:00
6bfff62513 fix: skip creating a directory in the container when it already exists 2024-09-16 08:57:44 -07:00
0b7cc51c6e Merge pull request #6 from jamesmurdza/fix-ghost-terminals
fix: ghost terminals, spam HTTP requests on dashboard
2024-09-16 08:57:13 -07:00
a353863523 fix: ghost terminals, spam HTTP requests on dashboard 2024-09-16 11:13:36 -04:00
2f88ff6d58 feat: speed up new project creation by copying files concurrently 2024-09-15 10:29:23 -07:00
48731848dd Merge branch 'refs/heads/main' into production 2024-09-15 10:25:44 -07:00
0509716f34 fix: select ReactJS template by default 2024-09-15 08:05:53 -07:00
3fcfe5a3dc Merge branch 'refs/heads/main' into production 2024-09-06 18:15:25 -07:00
06118e98e9 feat: remove the ai toggle switch 2024-09-06 18:14:54 -07:00
4ebd6dea96 fix: catch errors when copying files to the container 2024-09-06 18:14:11 -07:00
6e8eee246f Merge branch 'refs/heads/main' into production 2024-09-06 15:29:03 -07:00
8921cd83bb fix: encode line breaks when making requests to the AI generation worker 2024-09-06 15:28:36 -07:00
45097e0f20 fix: use latest instruction value when generating code 2024-09-06 15:28:33 -07:00
62e6d64a52 feat: change code generation to replace the selected code chunk and use Claude 3.5 Sonnet 2024-09-06 15:28:31 -07:00
982a6edc26 fix: encode line breaks when making requests to the AI generation worker 2024-09-06 15:27:02 -07:00
300de1f03a fix: use latest instruction value when generating code 2024-09-06 15:25:50 -07:00
02deea9c93 feat: change code generation to replace the selected code chunk and use Claude 3.5 Sonnet 2024-09-06 15:25:09 -07:00
653142dd1d Merge branch 'refs/heads/main' into production 2024-09-06 14:19:24 -07:00
0c6b2b0dfb feat: increase the per user limit of generations to 1000 2024-09-06 14:19:14 -07:00
31d74ddc2d Merge pull request #4 from Code-Victor/feat/ai-edit-selection-n-a11y
Feat/ai edit selection n a11y
2024-09-06 14:09:43 -07:00
62311faf51 feat: add AI edit code selection 2024-09-06 20:41:45 +01:00
208d17879f feat: add extra small btn variant 2024-09-06 20:07:29 +01:00
0067dc8c0c feat(a11y): make the generate input a form 2024-09-06 20:07:15 +01:00
ec24e64b17 Merge branch 'refs/heads/main' into production 2024-09-05 16:10:41 -07:00
4fe749daf2 Merge pull request #3 from Code-Victor/feat/syntax-highlighting-n-a11y
Feat/syntax highlighting n a11y
2024-09-05 16:09:23 -07:00
b8398cc4c2 chore: split up default terminal commands 2024-09-05 15:12:29 -07:00
0e4649b2c9 chore: add missing await 2024-09-05 15:08:46 -07:00
d74205c909 fix: remove unneeded pty.wait 2024-09-05 15:06:21 -07:00
fa998d9069 Merge branch 'refs/heads/main' into production 2024-09-05 14:26:35 -07:00
b01934bd20 fix: change to non-streaming input method for E2B terminals 2024-09-05 14:25:11 -07:00
a1990a189c chore: migrate E2B SDK to beta version 2024-09-05 14:24:54 -07:00
bf79893dfa feat(a11y): add Esc key functionality to close modal 2024-09-05 13:30:41 +01:00
47324f15bf feat: add support for syntax highlighting for 290+ languages 2024-09-05 13:30:24 +01:00
f5b04f9f49 Merge branch 'refs/heads/main' into production 2024-09-01 21:55:35 -07:00
7149925539 fix: remove useCallback, fixing null socket issue when reading files 2024-09-01 21:55:29 -07:00
169319de14 Merge branch 'refs/heads/main' into production 2024-09-01 19:31:46 -07:00
665e36603f Merge branch 'refs/heads/fix-files-loading' 2024-09-01 19:31:33 -07:00
0679f99bb7 fix: socket connection 2024-09-01 19:31:25 -07:00
2dbdf51fd3 Merge branch 'refs/heads/main' into production 2024-09-01 18:31:36 -07:00
2065814aaa Merge branch 'refs/heads/fix-files-loading'
# Conflicts:
#	frontend/components/editor/navbar/run.tsx
2024-09-01 18:31:15 -07:00
1502047bf2 fix: files not loading when creating a new project 2024-09-01 18:25:25 -07:00
7b2ed21288 chore: start to dev 2024-08-28 19:45:44 -07:00
bbd47db467 chore: start to dev 2024-08-28 19:45:35 -07:00
2da60ff4e4 fix: only one socket connection via socketcontext 2024-08-23 20:09:54 -04:00
f4a84bd4b6 Merge branch 'refs/heads/main' into production 2024-08-19 18:17:57 -07:00
ae7ff3f46b fix: types mismatch 2024-08-19 21:17:30 -04:00
171a9ce3c6 Merge branch 'refs/heads/main' into production 2024-08-19 17:51:02 -07:00
7559e9804f feat: different run commands based on file types 2024-08-19 20:39:04 -04:00
5132850cb0 fix: remove undefined type 2024-08-18 12:37:17 -07:00
26 changed files with 1313 additions and 454 deletions

View File

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

View File

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

View File

@ -1,43 +1,77 @@
import { Anthropic } from "@anthropic-ai/sdk";
export interface Env {
AI: any
ANTHROPIC_API_KEY: string;
}
export default {
async fetch(request, env): Promise<Response> {
if (request.method !== "GET") {
return new Response("Method Not Allowed", { status: 405 })
}
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method !== "GET") {
return new Response("Method Not Allowed", { status: 405 });
}
const url = new URL(request.url)
const fileName = url.searchParams.get("fileName")
const instructions = url.searchParams.get("instructions")
const line = url.searchParams.get("line")
const code = url.searchParams.get("code")
const url = new URL(request.url);
// const fileName = url.searchParams.get("fileName");
// const line = url.searchParams.get("line");
const instructions = url.searchParams.get("instructions");
const code = url.searchParams.get("code");
const response = await env.AI.run("@cf/meta/llama-3-8b-instruct", {
messages: [
{
role: "system",
content:
"You are an expert coding assistant. You read code from a file, and you suggest new code to add to the file. You may be given instructions on what to generate, which you should follow. You should generate code that is CORRECT, efficient, and follows best practices. You may generate multiple lines of code if necessary. When you generate code, you should ONLY return the code, and nothing else. You MUST NOT include backticks in the code you generate.",
},
{
role: "user",
content: `The file is called ${fileName}.`,
},
{
role: "user",
content: `Here are my instructions on what to generate: ${instructions}.`,
},
{
role: "user",
content: `Suggest me code to insert at line ${line} in my file. Give only the code, and NOTHING else. DO NOT include backticks in your response. My code file content is as follows
${code}`,
},
],
})
const prompt = `
Make the following changes to the code below:
- ${instructions}
return new Response(JSON.stringify(response))
},
} satisfies ExportedHandler<Env>
Return the complete code chunk. Do not refer to other code files. Do not add code before or after the chunk. Start your reponse with \`\`\`, and end with \`\`\`. Do not include any other text.
\`\`\`
${code}
\`\`\`
`;
console.log(prompt);
try {
const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY });
interface TextBlock {
type: "text";
text: string;
}
interface ToolUseBlock {
type: "tool_use";
tool_use: {
// Add properties if needed
};
}
type ContentBlock = TextBlock | ToolUseBlock;
function getTextContent(content: ContentBlock[]): string {
for (const block of content) {
if (block.type === "text") {
return block.text;
}
}
return "No text content found";
}
const response = await anthropic.messages.create({
model: "claude-3-5-sonnet-20240620",
max_tokens: 1024,
messages: [{ role: "user", content: prompt }],
});
const message = response.content as ContentBlock[];
const textBlockContent = getTextContent(message);
const pattern = /```[a-zA-Z]*\n([\s\S]*?)\n```/;
const match = textBlockContent.match(pattern);
const codeContent = match ? match[1] : "Error: Could not extract code.";
return new Response(JSON.stringify({ "response": codeContent }))
} catch (error) {
console.error("Error:", error);
return new Response("Internal Server Error", { status: 500 });
}
},
};

View File

@ -12,7 +12,7 @@
"concurrently": "^8.2.2",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"e2b": "^0.16.1",
"e2b": "^0.16.2-beta.47",
"express": "^4.19.2",
"rate-limiter-flexible": "^5.0.3",
"simple-git": "^3.25.0",
@ -41,6 +41,28 @@
"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": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@ -443,6 +465,7 @@
"integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
"hasInstallScript": true,
"optional": true,
"peer": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
@ -564,6 +587,11 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"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": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -737,23 +765,19 @@
}
},
"node_modules/e2b": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/e2b/-/e2b-0.16.2.tgz",
"integrity": "sha512-xKmVK4ipgVQPJ/uyyrfH9LnaawERRWt8U2UZhdhGfzdL/QU/OpBjuhoIbFCv1Uy6qXV4nIiJ6Nw4MBC4HmXf1g==",
"version": "0.16.2-beta.47",
"resolved": "https://registry.npmjs.org/e2b/-/e2b-0.16.2-beta.47.tgz",
"integrity": "sha512-tMPDYLMD+8+JyLPrsWft3NHBhK5YKOFOXzKMwpOKR5KvXOkd1silkArDwplmBUzN/eG/uRzWdtHZs9mHUQ5b9g==",
"dependencies": {
"isomorphic-ws": "^5.0.0",
"normalize-path": "^3.0.0",
"openapi-typescript-fetch": "^1.1.3",
"path-browserify": "^1.0.1",
"platform": "^1.3.6",
"ws": "^8.15.1"
"@bufbuild/protobuf": "^1.10.0",
"@connectrpc/connect": "^1.4.0",
"@connectrpc/connect-web": "^1.4.0",
"compare-versions": "^6.1.0",
"openapi-fetch": "^0.9.7",
"platform": "^1.3.6"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"bufferutil": "^4.0.8",
"utf-8-validate": "^6.0.3"
}
},
"node_modules/ee-first": {
@ -1195,14 +1219,6 @@
"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": {
"version": "4.17.21",
"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",
"integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==",
"optional": true,
"peer": true,
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
@ -1383,6 +1400,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -1417,15 +1435,19 @@
"node": ">= 0.8"
}
},
"node_modules/openapi-typescript-fetch": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/openapi-typescript-fetch/-/openapi-typescript-fetch-1.1.3.tgz",
"integrity": "sha512-smLZPck4OkKMNExcw8jMgrMOGgVGx2N/s6DbKL2ftNl77g5HfoGpZGFy79RBzU/EkaO0OZpwBnslfdBfh7ZcWg==",
"engines": {
"node": ">= 12.0.0",
"npm": ">= 7.0.0"
"node_modules/openapi-fetch": {
"version": "0.9.8",
"resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.9.8.tgz",
"integrity": "sha512-zM6elH0EZStD/gSiNlcPrzXcVQ/pZo3BDvC6CDwRDUt1dDzxlshpmQnpD6cZaJ39THaSmwVCxxRrPKNM1hHrDg==",
"dependencies": {
"openapi-typescript-helpers": "^0.0.8"
}
},
"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": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@ -1434,11 +1456,6 @@
"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": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
@ -2053,6 +2070,7 @@
"integrity": "sha512-xu9GQDeFp+eZ6LnCywXN/zBancWvOpUMzgjLPSjy4BRHSmTelvn2E0DG0o1sTiw5hkCKBHo8rwSKncfRfv2EEQ==",
"hasInstallScript": true,
"optional": true,
"peer": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
@ -2098,26 +2116,6 @@
"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": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

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

View File

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

View File

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

View File

@ -20,7 +20,11 @@ import {
saveFile,
} from "./fileoperations";
import { LockManager } from "./utils";
import { Sandbox, Terminal, FilesystemManager } from "e2b";
import { Sandbox, Filesystem } from "e2b";
import { Terminal } from "./Terminal"
import {
MAX_BODY_SIZE,
createFileRL,
@ -52,12 +56,12 @@ const terminals: Record<string, Terminal> = {};
const dirName = "/home/user";
const moveFile = async (
filesystem: FilesystemManager,
filesystem: Filesystem,
filePath: string,
newFilePath: string
) => {
const fileContents = await filesystem.readBytes(filePath);
await filesystem.writeBytes(newFilePath, fileContents);
const fileContents = await filesystem.read(filePath);
await filesystem.write(newFilePath, fileContents);
await filesystem.remove(filePath);
};
@ -155,8 +159,9 @@ io.on("connection", async (socket) => {
await lockManager.acquireLock(data.sandboxId, async () => {
try {
if (!containers[data.sandboxId]) {
containers[data.sandboxId] = await Sandbox.create();
// Start a new container if the container doesn't exist or it timed out.
if (!containers[data.sandboxId] || !(await containers[data.sandboxId].isRunning())) {
containers[data.sandboxId] = await Sandbox.create({ timeoutMs: 1200_000 });
console.log("Created container ", data.sandboxId);
}
} catch (e: any) {
@ -167,19 +172,28 @@ io.on("connection", async (socket) => {
// Change the owner of the project directory to user
const fixPermissions = async () => {
await containers[data.sandboxId].process.startAndWait(
`sudo chown -R user "${path.join(dirName, "projects", data.sandboxId)}"`
await containers[data.sandboxId].commands.run(
`sudo chown -R user "${path.posix.join(dirName, "projects", data.sandboxId)}"`
);
};
// Copy all files from the project to the container
const sandboxFiles = await getSandboxFiles(data.sandboxId);
sandboxFiles.fileData.forEach(async (file) => {
const filePath = path.join(dirName, file.id);
await containers[data.sandboxId].filesystem.makeDir(
path.dirname(filePath)
);
await containers[data.sandboxId].filesystem.write(filePath, file.data);
const containerFiles = containers[data.sandboxId].files;
const promises = sandboxFiles.fileData.map(async (file) => {
try {
const filePath = path.posix.join(dirName, file.id);
const parentDirectory = path.dirname(filePath);
if (!containerFiles.exists(parentDirectory)) {
await containerFiles.makeDir(parentDirectory);
}
await containerFiles.write(filePath, file.data);
} catch (e: any) {
console.log("Failed to create file: " + e);
}
});
await Promise.all(promises);
fixPermissions();
socket.emit("loaded", sandboxFiles.files);
@ -231,8 +245,8 @@ io.on("connection", async (socket) => {
if (!file) return;
file.data = body;
await containers[data.sandboxId].filesystem.write(
path.join(dirName, file.id),
await containers[data.sandboxId].files.write(
path.posix.join(dirName, file.id),
body
);
fixPermissions();
@ -253,9 +267,9 @@ io.on("connection", async (socket) => {
const newFileId = folderId + "/" + parts.pop();
await moveFile(
containers[data.sandboxId].filesystem,
path.join(dirName, fileId),
path.join(dirName, newFileId)
containers[data.sandboxId].files,
path.posix.join(dirName, fileId),
path.posix.join(dirName, newFileId)
);
fixPermissions();
@ -346,8 +360,8 @@ io.on("connection", async (socket) => {
const id = `projects/${data.sandboxId}/${name}`;
await containers[data.sandboxId].filesystem.write(
path.join(dirName, id),
await containers[data.sandboxId].files.write(
path.posix.join(dirName, id),
""
);
fixPermissions();
@ -383,8 +397,8 @@ io.on("connection", async (socket) => {
const id = `projects/${data.sandboxId}/${name}`;
await containers[data.sandboxId].filesystem.makeDir(
path.join(dirName, id)
await containers[data.sandboxId].files.makeDir(
path.posix.join(dirName, id)
);
callback();
@ -412,9 +426,9 @@ io.on("connection", async (socket) => {
parts.slice(0, parts.length - 1).join("/") + "/" + newName;
await moveFile(
containers[data.sandboxId].filesystem,
path.join(dirName, fileId),
path.join(dirName, newFileId)
containers[data.sandboxId].files,
path.posix.join(dirName, fileId),
path.posix.join(dirName, newFileId)
);
fixPermissions();
await renameFile(fileId, newFileId, file.data);
@ -435,8 +449,8 @@ io.on("connection", async (socket) => {
const file = sandboxFiles.fileData.find((f) => f.id === fileId);
if (!file) return;
await containers[data.sandboxId].filesystem.remove(
path.join(dirName, fileId)
await containers[data.sandboxId].files.remove(
path.posix.join(dirName, fileId)
);
sandboxFiles.fileData = sandboxFiles.fileData.filter(
(f) => f.id !== fileId
@ -462,8 +476,8 @@ io.on("connection", async (socket) => {
await Promise.all(
files.map(async (file) => {
await containers[data.sandboxId].filesystem.remove(
path.join(dirName, file)
await containers[data.sandboxId].files.remove(
path.posix.join(dirName, file)
);
sandboxFiles.fileData = sandboxFiles.fileData.filter(
@ -491,35 +505,42 @@ io.on("connection", async (socket) => {
await lockManager.acquireLock(data.sandboxId, async () => {
try {
terminals[id] = await containers[data.sandboxId].terminal.start({
onData: (responseData: string) => {
io.emit("terminalResponse", { id, data: responseData });
terminals[id] = new Terminal(containers[data.sandboxId])
await terminals[id].init({
onData: (responseString: string) => {
io.emit("terminalResponse", { id, data: responseString });
function extractPortNumber(inputString: string) {
// Remove ANSI escape codes
const cleanedString = inputString.replace(/\x1B\[[0-9;]*m/g, '');
// Regular expression to match port number
const regex = /http:\/\/localhost:(\d+)/;
// If a match is found, return the port number
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) {
io.emit(
"previewURL",
"https://" + containers[data.sandboxId].getHostname(port)
"https://" + containers[data.sandboxId].getHost(port)
);
}
},
size: { cols: 80, rows: 20 },
onExit: () => console.log("Terminal exited", id),
cols: 80,
rows: 20,
//onExit: () => console.log("Terminal exited", id),
});
await terminals[id].sendData(
`cd "${path.join(dirName, "projects", data.sandboxId)}"\r`
);
await terminals[id].sendData("export PS1='user> '\rclear\r");
const defaultDirectory = path.posix.join(dirName, "projects", data.sandboxId);
const defaultCommands = [
`cd "${defaultDirectory}"`,
"export PS1='user> '",
"clear"
]
for (const command of defaultCommands) await terminals[id].sendData(command + "\r");
console.log("Created terminal", id);
} catch (e: any) {
console.error(`Error creating terminal ${id}:`, e);
@ -548,13 +569,13 @@ io.on("connection", async (socket) => {
}
);
socket.on("terminalData", (id: string, data: string) => {
socket.on("terminalData", async (id: string, data: string) => {
try {
if (!terminals[id]) {
return;
}
terminals[id].sendData(data);
await terminals[id].sendData(data);
} catch (e: any) {
console.error("Error writing to terminal:", e);
io.emit("error", `Error: writing to terminal. ${e.message ?? e}`);
@ -567,7 +588,7 @@ io.on("connection", async (socket) => {
return;
}
await terminals[id].kill();
await terminals[id].close();
delete terminals[id];
callback();
@ -603,7 +624,7 @@ io.on("connection", async (socket) => {
// Generate code from cloudflare workers AI
const generateCodePromise = fetch(
`${process.env.AI_WORKER_URL}/api?fileName=${fileName}&code=${code}&line=${line}&instructions=${instructions}`,
`${process.env.AI_WORKER_URL}/api?fileName=${encodeURIComponent(fileName)}&code=${encodeURIComponent(code)}&line=${encodeURIComponent(line)}&instructions=${encodeURIComponent(instructions)}`,
{
headers: {
"Content-Type": "application/json",
@ -636,7 +657,7 @@ io.on("connection", async (socket) => {
if (data.isOwner && connections[data.sandboxId] <= 0) {
await Promise.all(
Object.entries(terminals).map(async ([key, terminal]) => {
await terminal.kill();
await terminal.close();
delete terminals[key];
})
);
@ -644,7 +665,7 @@ io.on("connection", async (socket) => {
await lockManager.acquireLock(data.sandboxId, async () => {
try {
if (containers[data.sandboxId]) {
await containers[data.sandboxId].close();
await containers[data.sandboxId].kill();
delete containers[data.sandboxId];
console.log("Closed container", data.sandboxId);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -6,8 +6,8 @@ import { ThemeProvider } from "@/components/layout/themeProvider"
import { ClerkProvider } from "@clerk/nextjs"
import { Toaster } from "@/components/ui/sonner"
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'
export const metadata: Metadata = {
title: "Sandbox",
@ -15,7 +15,7 @@ export const metadata: Metadata = {
}
export default function RootLayout({
children,
children
}: Readonly<{
children: React.ReactNode
}>) {
@ -29,11 +29,11 @@ export default function RootLayout({
forcedTheme="dark"
disableTransitionOnChange
>
<SocketProvider>
<PreviewProvider>
<TerminalProvider>
{children}
</TerminalProvider>
</PreviewProvider>
</SocketProvider>
<Analytics />
<Toaster position="bottom-left" richColors />
</ThemeProvider>

View File

@ -49,11 +49,9 @@ export default function Dashboard({
const q = searchParams.get("q")
const router = useRouter()
useEffect(() => {
if (!sandboxes) {
router.refresh()
}
}, [sandboxes])
useEffect(() => { // update the dashboard to show a new project
router.refresh()
}, [])
return (
<>

View File

@ -93,7 +93,7 @@ export default function NewProjectModal({
open: boolean
setOpen: (open: boolean) => void
}) {
const [selected, setSelected] = useState("react")
const [selected, setSelected] = useState("reactjs")
const [loading, setLoading] = useState(false)
const router = useRouter()

View File

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

View File

@ -1,11 +1,11 @@
"use client"
import { SetStateAction, useCallback, useEffect, useRef, useState } from "react"
import monaco from "monaco-editor"
import * as monaco from "monaco-editor"
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
import { Socket, io } from "socket.io-client"
import { toast } from "sonner"
import { useClerk } from "@clerk/nextjs"
import { AnimatePresence, motion } from "framer-motion"
import * as Y from "yjs"
import LiveblocksProvider from "@liveblocks/yjs"
@ -18,7 +18,7 @@ import {
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable"
import { FileJson, Loader2, TerminalSquare } from "lucide-react"
import { FileJson, Loader2, Sparkles, TerminalSquare } from "lucide-react"
import Tab from "../ui/tab"
import Sidebar from "./sidebar"
import GenerateInput from "./generate"
@ -31,8 +31,10 @@ import Loading from "./loading"
import PreviewWindow from "./preview"
import Terminals from "./terminals"
import { ImperativePanelHandle } from "react-resizable-panels"
import { PreviewProvider, usePreview } from '@/context/PreviewContext';
import { useTerminal } from '@/context/TerminalContext';
import { PreviewProvider, usePreview } from "@/context/PreviewContext"
import { useSocket } from "@/context/SocketContext"
import { Button } from "../ui/button"
import React from "react"
export default function CodeEditor({
userData,
@ -41,24 +43,19 @@ export default function CodeEditor({
userData: User
sandboxData: Sandbox
}) {
const socketRef = useRef<Socket | null>(null);
// Initialize socket connection if it doesn't exist
if (!socketRef.current) {
socketRef.current = io(
`${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userData.id}&sandboxId=${sandboxData.id}`,
{
timeout: 2000,
}
);
}
//Terminalcontext functionsand effects
const { setUserAndSandboxId } = useTerminal();
//SocketContext functions and effects
const { socket, setUserAndSandboxId } = useSocket()
useEffect(() => {
setUserAndSandboxId(userData.id, sandboxData.id);
}, [userData.id, sandboxData.id, setUserAndSandboxId]);
// Ensure userData.id and sandboxData.id are available before attempting to connect
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 Button state
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
@ -73,6 +70,8 @@ export default function CodeEditor({
const [activeFileId, setActiveFileId] = useState<string>("")
const [activeFileContent, setActiveFileContent] = useState("")
const [deletingFolderId, setDeletingFolderId] = useState("")
// Added this state to track the most recent content for each file
const [fileContents, setFileContents] = useState<Record<string, string>>({});
// Editor state
const [editorLanguage, setEditorLanguage] = useState("plaintext")
@ -81,7 +80,6 @@ export default function CodeEditor({
useState<monaco.editor.IStandaloneCodeEditor>()
// AI Copilot state
const [ai, setAi] = useState(false)
const [generate, setGenerate] = useState<{
show: boolean
id: string
@ -94,7 +92,8 @@ export default function CodeEditor({
options: monaco.editor.IModelDeltaDecoration[]
instance: monaco.editor.IEditorDecorationsCollection | undefined
}>({ options: [], instance: undefined })
const [isSelected, setIsSelected] = useState(false)
const [showSuggestion, setShowSuggestion] = useState(false)
// Terminal state
const [terminals, setTerminals] = useState<
{
@ -104,13 +103,13 @@ export default function CodeEditor({
>([])
// Preview state
const [previewURL, setPreviewURL] = useState<string>("");
const [previewURL, setPreviewURL] = useState<string>("")
const loadPreviewURL = (url: string) => {
// This will cause a reload if previewURL changed.
setPreviewURL(url);
setPreviewURL(url)
// If the URL didn't change, still reload the preview.
previewWindowRef.current?.refreshIframe();
previewWindowRef.current?.refreshIframe()
}
const isOwner = sandboxData.userId === userData.id
@ -120,26 +119,32 @@ export default function CodeEditor({
const room = useRoom()
const [provider, setProvider] = useState<TypedLiveblocksProvider>()
const userInfo = useSelf((me) => me.info)
// Liveblocks providers map to prevent reinitializing providers
type ProviderData = {
provider: LiveblocksProvider<never, never, never, never>;
yDoc: Y.Doc;
yText: Y.Text;
binding?: MonacoBinding;
onSync: (isSynced: boolean) => void;
};
const providersMap = useRef(new Map<string, ProviderData>());
provider: LiveblocksProvider<never, never, never, never>
yDoc: Y.Doc
yText: Y.Text
binding?: MonacoBinding
onSync: (isSynced: boolean) => void
}
const providersMap = useRef(new Map<string, ProviderData>())
// Refs for libraries / features
const editorContainerRef = useRef<HTMLDivElement>(null)
const monacoRef = useRef<typeof monaco | null>(null)
const generateRef = useRef<HTMLDivElement>(null)
const suggestionRef = useRef<HTMLDivElement>(null)
const generateWidgetRef = useRef<HTMLDivElement>(null)
const previewPanelRef = useRef<ImperativePanelHandle>(null)
const editorPanelRef = useRef<ImperativePanelHandle>(null)
const previewWindowRef = useRef<{ refreshIframe: () => void }>(null)
const debouncedSetIsSelected = useRef(
debounce((value: boolean) => {
setIsSelected(value)
}, 800) //
).current
// Pre-mount editor keybindings
const handleEditorWillMount: BeforeMount = (monaco) => {
monaco.editor.addKeybindingRules([
@ -156,6 +161,13 @@ export default function CodeEditor({
monacoRef.current = monaco
editor.onDidChangeCursorPosition((e) => {
setIsSelected(false)
const selection = editor.getSelection()
if (selection !== null) {
const hasSelection = !selection.isEmpty()
debouncedSetIsSelected(hasSelection)
setShowSuggestion(hasSelection)
}
const { column, lineNumber } = e.position
if (lineNumber === cursorLine) return
setCursorLine(lineNumber)
@ -209,28 +221,63 @@ export default function CodeEditor({
},
})
}
const handleAiEdit = React.useCallback(() => {
if (!editorRef) return
const selection = editorRef.getSelection()
if (!selection) return
const pos = selection.getPosition()
const start = selection.getStartPosition()
const end = selection.getEndPosition()
let pref: monaco.editor.ContentWidgetPositionPreference
let id = ""
const isMultiline = start.lineNumber !== end.lineNumber
if (isMultiline) {
if (pos.lineNumber <= start.lineNumber) {
pref = monaco.editor.ContentWidgetPositionPreference.ABOVE
} else {
pref = monaco.editor.ContentWidgetPositionPreference.BELOW
}
} else {
pref = monaco.editor.ContentWidgetPositionPreference.ABOVE
}
editorRef.changeViewZones(function (changeAccessor) {
if (!generateRef.current) return
if (pref === monaco.editor.ContentWidgetPositionPreference.ABOVE) {
id = changeAccessor.addZone({
afterLineNumber: start.lineNumber - 1,
heightInLines: 2,
domNode: generateRef.current,
})
}
})
setGenerate((prev) => {
return {
...prev,
show: true,
pref: [pref],
id,
}
})
}, [editorRef])
// Generate widget effect
useEffect(() => {
if (!ai) {
setGenerate((prev) => {
return {
...prev,
show: false,
}
})
return
}
if (generate.show) {
setShowSuggestion(false)
editorRef?.changeViewZones(function (changeAccessor) {
if (!generateRef.current) return
const id = changeAccessor.addZone({
afterLineNumber: cursorLine,
heightInLines: 3,
domNode: generateRef.current,
})
if (!generate.id) {
const id = changeAccessor.addZone({
afterLineNumber: cursorLine,
heightInLines: 3,
domNode: generateRef.current,
})
setGenerate((prev) => {
return { ...prev, id, line: cursorLine }
})
}
setGenerate((prev) => {
return { ...prev, id, line: cursorLine }
return { ...prev, line: cursorLine }
})
})
@ -291,6 +338,42 @@ export default function CodeEditor({
})
}
}, [generate.show])
// Suggestion widget effect
useEffect(() => {
if (!suggestionRef.current || !editorRef) return
const widgetElement = suggestionRef.current
const suggestionWidget: monaco.editor.IContentWidget = {
getDomNode: () => {
return widgetElement
},
getId: () => {
return "suggestion.widget"
},
getPosition: () => {
const selection = editorRef?.getSelection()
const column = Math.max(3, selection?.positionColumn ?? 1)
let lineNumber = selection?.positionLineNumber ?? 1
let pref = monaco.editor.ContentWidgetPositionPreference.ABOVE
if (lineNumber <= 3) {
pref = monaco.editor.ContentWidgetPositionPreference.BELOW
}
return {
preference: [pref],
position: {
lineNumber,
column,
},
}
},
}
if (isSelected) {
editorRef?.addContentWidget(suggestionWidget)
editorRef?.applyFontInfo(suggestionRef.current)
} else {
editorRef?.removeContentWidget(suggestionWidget)
}
}, [isSelected])
// Decorations effect for generate widget tips
useEffect(() => {
@ -298,14 +381,21 @@ export default function CodeEditor({
decorations.instance?.clear()
}
if (!ai) return
const model = editorRef?.getModel()
const line = model?.getLineContent(cursorLine)
if (line === undefined || line.trim() !== "") {
decorations.instance?.clear()
return
// added this because it was giving client side exception - Illegal value for lineNumber when opening an empty file
if (model) {
const totalLines = model.getLineCount();
// Check if the cursorLine is a valid number, If cursorLine is out of bounds, we fall back to 1 (the first line) as a default safe value.
const lineNumber = cursorLine > 0 && cursorLine <= totalLines ? cursorLine : 1; // fallback to a valid line number
// If for some reason the content doesn't exist, we use an empty string as a fallback.
const line = model.getLineContent(lineNumber) ?? "";
// Check if the line is not empty or only whitespace (i.e., `.trim()` removes spaces).
// If the line has content, we clear any decorations using the instance of the `decorations` object.
// Decorations refer to editor highlights, underlines, or markers, so this clears those if conditions are met.
if (line.trim() !== "") {
decorations.instance?.clear();
return
}
}
if (decorations.instance) {
@ -324,33 +414,41 @@ export default function CodeEditor({
}, [decorations.options])
// Save file keybinding logic effect
// Function to save the file content after a debounce period
const debouncedSaveData = useCallback(
debounce((value: string | undefined, activeFileId: string | undefined) => {
setTabs((prev) =>
prev.map((tab) =>
tab.id === activeFileId ? { ...tab, saved: true } : tab
)
);
console.log(`Saving file...${activeFileId}`);
console.log(`Saving file...${value}`);
socketRef.current?.emit("saveFile", activeFileId, value);
debounce((activeFileId: string | undefined) => {
if (activeFileId) {
// Get the current content of the file
const content = fileContents[activeFileId];
// Mark the file as saved in the tabs
setTabs((prev) =>
prev.map((tab) =>
tab.id === activeFileId ? { ...tab, saved: true } : tab
)
);
console.log(`Saving file...${activeFileId}`);
console.log(`Saving file...${content}`);
socket?.emit("saveFile", activeFileId, content);
}
}, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000),
[socketRef]
[socket, fileContents]
);
// Keydown event listener to trigger file save on Ctrl+S or Cmd+S
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
debouncedSaveData(editorRef?.getValue(), activeFileId);
e.preventDefault()
debouncedSaveData(activeFileId);
}
};
document.addEventListener("keydown", down);
}
document.addEventListener("keydown", down)
return () => {
document.removeEventListener("keydown", down);
};
}, [activeFileId, tabs, debouncedSaveData]);
document.removeEventListener("keydown", down)
}
}, [activeFileId, tabs, debouncedSaveData])
// Liveblocks live collaboration setup effect
useEffect(() => {
@ -359,13 +457,13 @@ export default function CodeEditor({
if (!editorRef || !tab || !model) return
let providerData: ProviderData;
let providerData: ProviderData
// When a file is opened for the first time, create a new provider and store in providersMap.
if (!providersMap.current.has(tab.id)) {
const yDoc = new Y.Doc();
const yText = yDoc.getText(tab.id);
const yProvider = new LiveblocksProvider(room, yDoc);
const yDoc = new Y.Doc()
const yText = yDoc.getText(tab.id)
const yProvider = new LiveblocksProvider(room, yDoc)
// Inserts the file content into the editor once when the tab is changed.
const onSync = (isSynced: boolean) => {
@ -386,12 +484,11 @@ export default function CodeEditor({
yProvider.on("sync", onSync)
// Save the provider to the map.
providerData = { provider: yProvider, yDoc, yText, onSync };
providersMap.current.set(tab.id, providerData);
providerData = { provider: yProvider, yDoc, yText, onSync }
providersMap.current.set(tab.id, providerData)
} else {
// When a tab is opened that has been open before, reuse the existing provider.
providerData = providersMap.current.get(tab.id)!;
providerData = providersMap.current.get(tab.id)!
}
const binding = new MonacoBinding(
@ -399,21 +496,21 @@ export default function CodeEditor({
model,
new Set([editorRef]),
providerData.provider.awareness as unknown as Awareness
);
)
providerData.binding = binding;
setProvider(providerData.provider);
providerData.binding = binding
setProvider(providerData.provider)
return () => {
// Cleanup logic
if (binding) {
binding.destroy();
binding.destroy()
}
if (providerData.binding) {
providerData.binding = undefined;
providerData.binding = undefined
}
};
}, [room, activeFileContent]);
}
}, [room, activeFileContent])
// Added this effect to clean up when the component unmounts
useEffect(() => {
@ -421,22 +518,22 @@ export default function CodeEditor({
// Clean up all providers when the component unmounts
providersMap.current.forEach((data) => {
if (data.binding) {
data.binding.destroy();
data.binding.destroy()
}
data.provider.disconnect();
data.yDoc.destroy();
});
providersMap.current.clear();
};
}, []);
data.provider.disconnect()
data.yDoc.destroy()
})
providersMap.current.clear()
}
}, [])
// Connection/disconnection effect
useEffect(() => {
socketRef.current?.connect()
socket?.connect()
return () => {
socketRef.current?.disconnect()
socket?.disconnect()
}
}, [])
}, [socket])
// Socket event listener effect
useEffect(() => {
@ -469,67 +566,87 @@ export default function CodeEditor({
})
}
socketRef.current?.on("connect", onConnect)
socketRef.current?.on("disconnect", onDisconnect)
socketRef.current?.on("loaded", onLoadedEvent)
socketRef.current?.on("error", onError)
socketRef.current?.on("terminalResponse", onTerminalResponse)
socketRef.current?.on("disableAccess", onDisableAccess)
socketRef.current?.on("previewURL", loadPreviewURL)
socket?.on("connect", onConnect)
socket?.on("disconnect", onDisconnect)
socket?.on("loaded", onLoadedEvent)
socket?.on("error", onError)
socket?.on("terminalResponse", onTerminalResponse)
socket?.on("disableAccess", onDisableAccess)
socket?.on("previewURL", loadPreviewURL)
return () => {
socketRef.current?.off("connect", onConnect)
socketRef.current?.off("disconnect", onDisconnect)
socketRef.current?.off("loaded", onLoadedEvent)
socketRef.current?.off("error", onError)
socketRef.current?.off("terminalResponse", onTerminalResponse)
socketRef.current?.off("disableAccess", onDisableAccess)
socketRef.current?.off("previewURL", loadPreviewURL)
socket?.off("connect", onConnect)
socket?.off("disconnect", onDisconnect)
socket?.off("loaded", onLoadedEvent)
socket?.off("error", onError)
socket?.off("terminalResponse", onTerminalResponse)
socket?.off("disableAccess", onDisableAccess)
socket?.off("previewURL", loadPreviewURL)
}
// }, []);
}, [terminals])
}, [
socket,
terminals,
setTerminals,
setFiles,
toast,
setDisableAccess,
isOwner,
loadPreviewURL,
])
// Helper functions for tabs:
// Select file and load content
// Initialize debounced function once
const fileCache = useRef(new Map());
const fileCache = useRef(new Map())
// Debounced function to get file content
const debouncedGetFile = useCallback(
debounce((tabId, callback) => {
socketRef.current?.emit('getFile', tabId, callback);
}, 300), // 300ms debounce delay, adjust as needed
[]
);
const debouncedGetFile = (tabId: any, callback: any) => {
socket?.emit("getFile", tabId, callback)
} // 300ms debounce delay, adjust as needed
const selectFile = useCallback((tab: TTab) => {
const selectFile = (tab: TTab) => {
if (tab.id === activeFileId) return;
setGenerate((prev) => ({ ...prev, show: false }));
// Check if the tab already exists in the list of open tabs
const exists = tabs.find((t) => t.id === tab.id);
setTabs((prev) => {
if (exists) {
// If the tab exists, make it the active tab
setActiveFileId(exists.id);
return prev;
}
// If the tab doesn't exist, add it to the list of tabs and make it active
return [...prev, tab];
});
if (fileCache.current.has(tab.id)) {
setActiveFileContent(fileCache.current.get(tab.id));
// If the file's content is already cached, set it as the active content
if (fileContents[tab.id]) {
setActiveFileContent(fileContents[tab.id]);
} else {
debouncedGetFile(tab.id, (response: SetStateAction<string>) => {
fileCache.current.set(tab.id, response);
// Otherwise, fetch the content of the file and cache it
debouncedGetFile(tab.id, (response: string) => {
setFileContents(prev => ({ ...prev, [tab.id]: response }));
setActiveFileContent(response);
});
}
// Set the editor language based on the file type
setEditorLanguage(processFileType(tab.name));
// Set the active file ID to the new tab
setActiveFileId(tab.id);
}, [activeFileId, tabs, debouncedGetFile]);
};
// Added this effect to update fileContents when the editor content changes
useEffect(() => {
if (activeFileId) {
// Cache the current active file content using the file ID as the key
setFileContents(prev => ({ ...prev, [activeFileId]: activeFileContent }));
}
}, [activeFileContent, activeFileId]);
// Close tab and remove from tabs
const closeTab = (id: string) => {
@ -603,7 +720,7 @@ export default function CodeEditor({
return false
}
socketRef.current?.emit("renameFile", id, newName)
socket?.emit("renameFile", id, newName)
setTabs((prev) =>
prev.map((tab) => (tab.id === id ? { ...tab, name: newName } : tab))
)
@ -612,7 +729,7 @@ export default function CodeEditor({
}
const handleDeleteFile = (file: TFile) => {
socketRef.current?.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => {
socket?.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => {
setFiles(response)
})
closeTab(file.id)
@ -622,11 +739,11 @@ export default function CodeEditor({
setDeletingFolderId(folder.id)
console.log("deleting folder", folder.id)
socketRef.current?.emit("getFolder", folder.id, (response: string[]) =>
socket?.emit("getFolder", folder.id, (response: string[]) =>
closeTabs(response)
)
socketRef.current?.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => {
socket?.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => {
setFiles(response)
setDeletingFolderId("")
})
@ -650,30 +767,74 @@ export default function CodeEditor({
{/* Copilot DOM elements */}
<PreviewProvider>
<div ref={generateRef} />
<div ref={suggestionRef} className="absolute">
<AnimatePresence>
{isSelected && showSuggestion && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ ease: "easeOut", duration: 0.2 }}
>
<Button size="xs" type="submit" onClick={handleAiEdit}>
<Sparkles className="h-3 w-3 mr-1" />
Edit Code
</Button>
</motion.div>
)}
</AnimatePresence>
</div>
<div className="z-50 p-1" ref={generateWidgetRef}>
{generate.show && ai ? (
{generate.show ? (
<GenerateInput
user={userData}
socket={socketRef.current}
socket={socket!}
width={generate.width - 90}
data={{
fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "",
code: editorRef?.getValue() ?? "",
code:
(isSelected && editorRef?.getSelection()
? editorRef
?.getModel()
?.getValueInRange(editorRef?.getSelection()!)
: editorRef?.getValue()) ?? "",
line: generate.line,
}}
editor={{
language: editorLanguage,
}}
onExpand={() => {
const line = generate.line
editorRef?.changeViewZones(function (changeAccessor) {
changeAccessor.removeZone(generate.id)
if (!generateRef.current) return
const id = changeAccessor.addZone({
afterLineNumber: cursorLine,
heightInLines: 12,
domNode: generateRef.current,
})
let id = ""
if (isSelected) {
const selection = editorRef?.getSelection()
if (!selection) return
const isAbove =
generate.pref?.[0] ===
monaco.editor.ContentWidgetPositionPreference.ABOVE
const afterLineNumber = isAbove ? line - 1 : line
id = changeAccessor.addZone({
afterLineNumber,
heightInLines: isAbove ? 11 : 12,
domNode: generateRef.current,
})
const contentWidget = generate.widget
if (contentWidget) {
editorRef?.layoutContentWidget(contentWidget)
}
} else {
id = changeAccessor.addZone({
afterLineNumber: cursorLine,
heightInLines: 12,
domNode: generateRef.current,
})
}
setGenerate((prev) => {
return { ...prev, id }
})
@ -687,12 +848,14 @@ export default function CodeEditor({
show: !prev.show,
}
})
const file = editorRef?.getValue()
const lines = file?.split("\n") || []
lines.splice(line - 1, 0, code)
const updatedFile = lines.join("\n")
editorRef?.setValue(updatedFile)
const selection = editorRef?.getSelection()
const range =
isSelected && selection
? selection
: new monaco.Range(line, 1, line, 1)
editorRef?.executeEdits("ai-generation", [
{ range, text: code, forceMoveMarkers: true },
])
}}
onClose={() => {
setGenerate((prev) => {
@ -714,13 +877,10 @@ export default function CodeEditor({
handleRename={handleRename}
handleDeleteFile={handleDeleteFile}
handleDeleteFolder={handleDeleteFolder}
socket={socketRef.current}
socket={socket!}
setFiles={setFiles}
addNew={(name, type) => addNew(name, type, setFiles, sandboxData)}
deletingFolderId={deletingFolderId}
// AI Copilot Toggle
ai={ai}
setAi={setAi}
/>
{/* Shadcn resizeable panels: https://ui.shadcn.com/docs/components/resizable */}
@ -772,15 +932,10 @@ export default function CodeEditor({
beforeMount={handleEditorWillMount}
onMount={handleEditorMount}
onChange={(value) => {
if (value === activeFileContent) {
setTabs((prev) =>
prev.map((tab) =>
tab.id === activeFileId
? { ...tab, saved: true }
: tab
)
)
} else {
// If the new content is different from the cached content, update it
if (value !== fileContents[activeFileId]) {
setActiveFileContent(value ?? ""); // Update the active file content
// Mark the file as unsaved by setting 'saved' to false
setTabs((prev) =>
prev.map((tab) =>
tab.id === activeFileId
@ -788,6 +943,15 @@ export default function CodeEditor({
: tab
)
)
} else {
// If the content matches the cached content, mark the file as saved
setTabs((prev) =>
prev.map((tab) =>
tab.id === activeFileId
? { ...tab, saved: true }
: tab
)
)
}
}}
options={{
@ -832,7 +996,11 @@ export default function CodeEditor({
open={() => {
usePreview().previewPanelRef.current?.expand()
setIsPreviewCollapsed(false)
} } collapsed={isPreviewCollapsed} src={previewURL} ref={previewWindowRef} />
}}
collapsed={isPreviewCollapsed}
src={previewURL}
ref={previewWindowRef}
/>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel
@ -856,4 +1024,3 @@ export default function CodeEditor({
</>
)
}

View File

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

View File

@ -32,8 +32,6 @@ export default function Sidebar({
socket,
setFiles,
addNew,
ai,
setAi,
deletingFolderId,
}: {
sandboxData: Sandbox;
@ -50,8 +48,6 @@ export default function Sidebar({
socket: Socket;
setFiles: (files: (TFile | TFolder)[]) => void;
addNew: (name: string, type: "file" | "folder") => void;
ai: boolean;
setAi: React.Dispatch<React.SetStateAction<boolean>>;
deletingFolderId: string;
}) {
const ref = useRef(null); // drop target
@ -186,20 +182,6 @@ export default function Sidebar({
</div>
</div>
<div className="w-full space-y-4">
<div className="flex items-center justify-between w-full">
<div className="flex items-center">
<Sparkles
className={`h-4 w-4 mr-2 ${
ai ? "text-indigo-500" : "text-muted-foreground"
}`}
/>
Copilot{" "}
<span className="font-mono text-muted-foreground inline-block ml-1.5 text-xs leading-none border border-b-2 border-muted-foreground py-1 px-1.5 rounded-md">
G
</span>
</div>
<Switch checked={ai} onCheckedChange={setAi} />
</div>
{/* <Button className="w-full">
<MonitorPlay className="w-4 h-4 mr-2" /> Run
</Button> */}

View File

@ -8,12 +8,15 @@ import { toast } from "sonner";
import EditorTerminal from "./terminal";
import { useTerminal } from "@/context/TerminalContext";
import { useEffect } from "react";
import { useSocket } from "@/context/SocketContext"
export default function Terminals() {
const { socket } = useSocket();
const {
terminals,
setTerminals,
socket,
createNewTerminal,
closeTerminal,
activeTerminalId,

View File

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

View File

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

View File

@ -0,0 +1,63 @@
"use client";
import React, { createContext, useContext, useEffect, useState } from 'react';
import { io, Socket } from 'socket.io-client';
interface SocketContextType {
socket: Socket | null;
setUserAndSandboxId: (userId: string, sandboxId: string) => void;
}
const SocketContext = createContext<SocketContextType | undefined>(undefined);
export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [socket, setSocket] = useState<Socket | null>(null);
const [userId, setUserId] = useState<string | null>(null);
const [sandboxId, setSandboxId] = useState<string | null>(null);
useEffect(() => {
if (userId && sandboxId) {
console.log("Initializing socket connection...");
const newSocket = io(`${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userId}&sandboxId=${sandboxId}`);
console.log("Socket instance:", newSocket);
setSocket(newSocket);
newSocket.on('connect', () => {
console.log("Socket connected:", newSocket.id);
});
newSocket.on('disconnect', () => {
console.log("Socket disconnected");
});
return () => {
console.log("Disconnecting socket...");
newSocket.disconnect();
};
}
}, [userId, sandboxId]);
const setUserAndSandboxId = (newUserId: string, newSandboxId: string) => {
setUserId(newUserId);
setSandboxId(newSandboxId);
};
const value = {
socket,
setUserAndSandboxId,
};
return (
<SocketContext.Provider value={ value }>
{children}
</SocketContext.Provider>
);
};
export const useSocket = (): SocketContextType => {
const context = useContext(SocketContext);
if (!context) {
throw new Error('useSocket must be used within a SocketProvider');
}
return context;
};

View File

@ -1,12 +1,11 @@
"use client";
import React, { createContext, useContext, useState, useEffect } from 'react';
import { io, Socket } from 'socket.io-client';
import React, { createContext, useContext, useState } from 'react';
import { Terminal } from '@xterm/xterm';
import { createTerminal as createTerminalHelper, closeTerminal as closeTerminalHelper } from '@/lib/terminal';
import { useSocket } from '@/context/SocketContext';
interface TerminalContextType {
socket: Socket | null;
terminals: { id: string; terminal: Terminal | null }[];
setTerminals: React.Dispatch<React.SetStateAction<{ id: string; terminal: Terminal | null }[]>>;
activeTerminalId: string;
@ -15,41 +14,16 @@ interface TerminalContextType {
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>;
createNewTerminal: (command?: string) => Promise<void>;
closeTerminal: (id: string) => void;
setUserAndSandboxId: (userId: string, sandboxId: string) => void;
deploy: (callback: () => void) => void;
}
const TerminalContext = createContext<TerminalContextType | undefined>(undefined);
export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [socket, setSocket] = useState<Socket | null>(null);
const { socket } = useSocket();
const [terminals, setTerminals] = useState<{ id: string; terminal: Terminal | null }[]>([]);
const [activeTerminalId, setActiveTerminalId] = useState<string>('');
const [creatingTerminal, setCreatingTerminal] = useState<boolean>(false);
const [userId, setUserId] = useState<string | null>(null);
const [sandboxId, setSandboxId] = useState<string | null>(null);
useEffect(() => {
if (userId && sandboxId) {
console.log("Initializing socket connection...");
const newSocket = io(`${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userId}&sandboxId=${sandboxId}`);
console.log("Socket instance:", newSocket);
setSocket(newSocket);
newSocket.on('connect', () => {
console.log("Socket connected:", newSocket.id);
});
newSocket.on('disconnect', () => {
console.log("Socket disconnected");
});
return () => {
console.log("Disconnecting socket...");
newSocket.disconnect();
};
}
}, [userId, sandboxId]);
const createNewTerminal = async (command?: string): Promise<void> => {
if (!socket) return;
@ -85,11 +59,6 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil
}
};
const setUserAndSandboxId = (newUserId: string, newSandboxId: string) => {
setUserId(newUserId);
setSandboxId(newSandboxId);
};
const deploy = (callback: () => void) => {
if (!socket) console.error("Couldn't deploy: No socket");
console.log("Deploying...")
@ -99,7 +68,6 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil
}
const value = {
socket,
terminals,
setTerminals,
activeTerminalId,
@ -108,7 +76,6 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil
setCreatingTerminal,
createNewTerminal,
closeTerminal,
setUserAndSandboxId,
deploy
};

View 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"
}

View File

@ -2,18 +2,19 @@ import { type ClassValue, clsx } from "clsx"
// import { toast } from "sonner"
import { twMerge } from "tailwind-merge"
import { Sandbox, TFile, TFolder } from "./types"
import fileExtToLang from "./file-extension-to-language.json"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function processFileType(file: string) {
const ending = file.split(".").pop()
const extension = file.split(".").pop()
const fileExtToLangMap = fileExtToLang as Record<string, string>
if (extension && fileExtToLangMap[extension]) {
return fileExtToLangMap[extension]
}
if (ending === "ts" || ending === "tsx") return "typescript"
if (ending === "js" || ending === "jsx") return "javascript"
if (ending) return ending
return "plaintext"
}
@ -62,12 +63,15 @@ export function addNew(
}
}
export function debounce<T extends (...args: any[]) => void>(func: T, wait: number): T {
let timeout: NodeJS.Timeout | null = null;
export function debounce<T extends (...args: any[]) => void>(
func: T,
wait: number
): T {
let timeout: NodeJS.Timeout | null = null
return function (...args: Parameters<T>) {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => func(...args), wait);
} as T;
}
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(() => func(...args), wait)
} as T
}