Compare commits

...

23 Commits

Author SHA1 Message Date
023b3bdc5e fix: add missing await keywords 2024-09-30 04:20:14 -07:00
13be78dee8 fix: don't exit the script when exceptions occur 2024-09-30 02:55:30 -07:00
7a00d24ab9 feat: sync changes to the filesystem 2024-09-30 02:55:28 -07:00
69b1287349 fix: handle errors when fixing permissions 2024-09-29 17:40:09 -07:00
c94678c430 feat: watch container for file changes 2024-09-15 13:11:59 -07:00
585dcb469e fix: skip creating a directory in the container when it already exists 2024-09-15 10:47:00 -07:00
2f88ff6d58 feat: speed up new project creation by copying files concurrently 2024-09-15 10:29:23 -07:00
0509716f34 fix: select ReactJS template by default 2024-09-15 08:05:53 -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
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
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
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
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
18 changed files with 1306 additions and 370 deletions

View File

@ -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",

View File

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

View File

@ -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}
} satisfies ExportedHandler<Env> \`\`\`
`;
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", "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",

View File

@ -14,7 +14,7 @@
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"e2b": "^0.16.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",

View 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();

View File

@ -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);
) => { await filesystem.remove(filePath);
const fileContents = await filesystem.readBytes(filePath); } catch (e) {
await filesystem.writeBytes(newFilePath, fileContents); console.error(`Error moving file from ${filePath} to ${newFilePath}:`, e);
await filesystem.remove(filePath); }
}; };
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) {
fixPermissions(); console.log("Failed to check if directory: " + e);
return false;
}
};
// Only continue to container setup if a new container was created
if (createdContainer) {
// Copy all files from the project to the container
const promises = sandboxFiles.fileData.map(async (file) => {
try {
const filePath = path.join(dirName, file.id);
const parentDirectory = path.dirname(filePath);
if (!containerFiles.exists(parentDirectory)) {
await containerFiles.makeDir(parentDirectory);
}
await containerFiles.write(filePath, file.data);
} catch (e: any) {
console.log("Failed to create file: " + e);
}
});
await Promise.all(promises);
// Make the logged in user the owner of all project files
fixPermissions(projectDirectory);
}
// Start filesystem watcher for the project directory
const watchDirectory = async (directory: string) => {
try {
await containerFiles.watch(directory, async (event: FilesystemEvent) => {
try {
function removeDirName(path : string, dirName : string) {
return path.startsWith(dirName) ? path.slice(dirName.length) : path;
}
// This is the absolute file path in the container
const containerFilePath = path.join(directory, event.name);
// This is the file path relative to the home directory
const sandboxFilePath = removeDirName(containerFilePath, dirName + "/");
// This is the directory being watched relative to the home directory
const sandboxDirectory = removeDirName(directory, dirName + "/");
// Helper function to find a folder by id
function findFolderById(files: (TFolder | TFile)[], folderId : string) {
return files.find((file : TFolder | TFile) => file.type === "folder" && file.id === folderId);
}
// A new file or directory was created.
if (event.type === "create") {
const folder = findFolderById(sandboxFiles.files, sandboxDirectory) as TFolder;
const isDir = await isDirectory(containerFilePath);
const newItem = isDir
? { id: sandboxFilePath, name: event.name, type: "folder", children: [] } as TFolder
: { id: sandboxFilePath, name: event.name, type: "file" } as TFile;
if (folder) {
// If the folder exists, add the new item (file/folder) as a child
folder.children.push(newItem);
} else {
// If folder doesn't exist, add the new item to the root
sandboxFiles.files.push(newItem);
}
if (!isDir) {
const fileData = await containers[data.sandboxId].files.read(containerFilePath);
const fileContents = typeof fileData === "string" ? fileData : "";
sandboxFiles.fileData.push({ id: sandboxFilePath, data: fileContents });
}
console.log(`Create ${sandboxFilePath}`);
}
// A file or directory was removed or renamed.
else if (event.type === "remove" || event.type == "rename") {
const folder = findFolderById(sandboxFiles.files, sandboxDirectory) as TFolder;
const isDir = await isDirectory(containerFilePath);
const isFileMatch = (file: TFolder | TFile | TFileData) => file.id === sandboxFilePath || file.id.startsWith(containerFilePath + '/');
if (folder) {
// Remove item from its parent folder
folder.children = folder.children.filter((file: TFolder | TFile) => !isFileMatch(file));
} else {
// Remove from the root if it's not inside a folder
sandboxFiles.files = sandboxFiles.files.filter((file: TFolder | TFile) => !isFileMatch(file));
}
// Also remove any corresponding file data
sandboxFiles.fileData = sandboxFiles.fileData.filter((file: TFileData) => !isFileMatch(file));
console.log(`Removed: ${sandboxFilePath}`);
}
// The contents of a file were changed.
else if (event.type === "write") {
const folder = findFolderById(sandboxFiles.files, sandboxDirectory) as TFolder;
const fileToWrite = sandboxFiles.fileData.find(file => file.id === sandboxFilePath);
if (fileToWrite) {
fileToWrite.data = await containers[data.sandboxId].files.read(containerFilePath);
console.log(`Write to ${sandboxFilePath}`);
} else {
// If the file is part of a folder structure, locate it and update its data
const fileInFolder = folder?.children.find(file => file.id === sandboxFilePath);
if (fileInFolder) {
const fileData = await containers[data.sandboxId].files.read(containerFilePath);
const fileContents = typeof fileData === "string" ? fileData : "";
sandboxFiles.fileData.push({ id: sandboxFilePath, data: fileContents });
console.log(`Write to ${sandboxFilePath}`);
}
}
}
// Tell the client to reload the file list
socket.emit("loaded", sandboxFiles.files);
} catch (error) {
console.error(`Error handling ${event.type} event for ${event.name}:`, error);
}
})
} catch (error) {
console.error(`Error watching filesystem:`, error);
}
};
// Watch the project directory
await watchDirectory(projectDirectory);
// Watch all subdirectories of the project directory, but not deeper
// This also means directories created after the container is created won't be watched
const dirContent = await containerFiles.list(projectDirectory);
await Promise.all(dirContent.map(async (item : EntryInfo) => {
if (item.type === "dir") {
console.log("Watching " + item.path);
await watchDirectory(item.path);
}
}))
socket.emit("loaded", sandboxFiles.files); socket.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);
} }

View File

@ -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"
}, },

View File

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

View File

@ -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 {

View File

@ -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()

View File

@ -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">

View File

@ -1,10 +1,11 @@
"use client" "use client"
import { SetStateAction, useCallback, useEffect, useRef, useState } from "react" 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 Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
import { toast } from "sonner" import { toast } from "sonner"
import { useClerk } from "@clerk/nextjs" import { useClerk } from "@clerk/nextjs"
import { AnimatePresence, motion } from "framer-motion"
import * as Y from "yjs" import * as Y from "yjs"
import LiveblocksProvider from "@liveblocks/yjs" import LiveblocksProvider from "@liveblocks/yjs"
@ -17,7 +18,7 @@ import {
ResizablePanel, ResizablePanel,
ResizablePanelGroup, ResizablePanelGroup,
} from "@/components/ui/resizable" } 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 Tab from "../ui/tab"
import Sidebar from "./sidebar" import Sidebar from "./sidebar"
import GenerateInput from "./generate" import GenerateInput from "./generate"
@ -30,8 +31,10 @@ import Loading from "./loading"
import PreviewWindow from "./preview" import PreviewWindow from "./preview"
import Terminals from "./terminals" import Terminals from "./terminals"
import { ImperativePanelHandle } from "react-resizable-panels" import { ImperativePanelHandle } from "react-resizable-panels"
import { PreviewProvider, usePreview } from '@/context/PreviewContext'; import { PreviewProvider, usePreview } from "@/context/PreviewContext"
import { useSocket } from "@/context/SocketContext" import { useSocket } from "@/context/SocketContext"
import { Button } from "../ui/button"
import React from "react"
export default function CodeEditor({ export default function CodeEditor({
userData, userData,
@ -40,9 +43,8 @@ export default function CodeEditor({
userData: User userData: User
sandboxData: Sandbox sandboxData: Sandbox
}) { }) {
//SocketContext functions and effects //SocketContext functions and effects
const { socket, setUserAndSandboxId } = useSocket(); const { socket, setUserAndSandboxId } = useSocket()
useEffect(() => { useEffect(() => {
// Ensure userData.id and sandboxData.id are available before attempting to connect // Ensure userData.id and sandboxData.id are available before attempting to connect
@ -50,10 +52,10 @@ export default function CodeEditor({
// Check if the socket is not initialized or not connected // Check if the socket is not initialized or not connected
if (!socket || (socket && !socket.connected)) { if (!socket || (socket && !socket.connected)) {
// Initialize socket connection // Initialize socket connection
setUserAndSandboxId(userData.id, sandboxData.id); setUserAndSandboxId(userData.id, sandboxData.id)
} }
} }
}, [socket, userData.id, sandboxData.id, setUserAndSandboxId]); }, [socket, userData.id, sandboxData.id, setUserAndSandboxId])
//Preview Button state //Preview Button state
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true) const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
@ -76,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
@ -89,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<
{ {
@ -99,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
@ -118,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([
@ -151,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)
@ -204,28 +219,62 @@ export default function CodeEditor({
}, },
}) })
} }
const handleAiEdit = React.useCallback(() => {
// Generate widget effect if (!editorRef) return
useEffect(() => { const selection = editorRef.getSelection()
if (!ai) { if (!selection) return
setGenerate((prev) => { const pos = selection.getPosition()
return { const start = selection.getStartPosition()
...prev, const end = selection.getEndPosition()
show: false, let pref: monaco.editor.ContentWidgetPositionPreference
} let id = ""
}) const isMultiline = start.lineNumber !== end.lineNumber
return if (isMultiline) {
if (pos.lineNumber <= start.lineNumber) {
pref = monaco.editor.ContentWidgetPositionPreference.ABOVE
} else {
pref = monaco.editor.ContentWidgetPositionPreference.BELOW
}
} else {
pref = monaco.editor.ContentWidgetPositionPreference.ABOVE
} }
if (generate.show) { editorRef.changeViewZones(function (changeAccessor) {
editorRef?.changeViewZones(function (changeAccessor) { if (!generateRef.current) return
if (!generateRef.current) return if (pref === monaco.editor.ContentWidgetPositionPreference.ABOVE) {
const id = changeAccessor.addZone({ id = changeAccessor.addZone({
afterLineNumber: cursorLine, afterLineNumber: start.lineNumber - 1,
heightInLines: 3, heightInLines: 2,
domNode: generateRef.current, domNode: generateRef.current,
}) })
}
})
setGenerate((prev) => {
return {
...prev,
show: true,
pref: [pref],
id,
}
})
}, [editorRef])
// Generate widget effect
useEffect(() => {
if (generate.show) {
setShowSuggestion(false)
editorRef?.changeViewZones(function (changeAccessor) {
if (!generateRef.current) return
if (!generate.id) {
const id = changeAccessor.addZone({
afterLineNumber: cursorLine,
heightInLines: 3,
domNode: generateRef.current,
})
setGenerate((prev) => {
return { ...prev, id, line: cursorLine }
})
}
setGenerate((prev) => { setGenerate((prev) => {
return { ...prev, id, line: cursorLine } return { ...prev, line: cursorLine }
}) })
}) })
@ -286,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(() => {
@ -293,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)
@ -325,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(() => {
@ -354,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) => {
@ -381,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(
@ -394,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(() => {
@ -416,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(() => {
@ -435,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([])
@ -481,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) => {
@ -538,8 +626,8 @@ export default function CodeEditor({
? numTabs === 1 ? numTabs === 1
? null ? null
: index < numTabs - 1 : index < numTabs - 1
? tabs[index + 1].id ? tabs[index + 1].id
: tabs[index - 1].id : tabs[index - 1].id
: activeFileId : activeFileId
setTabs((prev) => prev.filter((t) => t.id !== id)) setTabs((prev) => prev.filter((t) => t.id !== id))
@ -632,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 />
</> </>
@ -643,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()
domNode: generateRef.current, 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) => { setGenerate((prev) => {
return { ...prev, id } return { ...prev, id }
}) })
@ -680,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) => {
@ -711,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 */}
@ -754,58 +885,58 @@ export default function CodeEditor({
</div> </div>
</> </>
) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643 ) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643
clerk.loaded ? ( clerk.loaded ? (
<> <>
{provider && userInfo ? ( {provider && userInfo ? (
<Cursors yProvider={provider} userInfo={userInfo} /> <Cursors yProvider={provider} userInfo={userInfo} />
) : null} ) : null}
<Editor <Editor
height="100%" height="100%"
language={editorLanguage} language={editorLanguage}
beforeMount={handleEditorWillMount} beforeMount={handleEditorWillMount}
onMount={handleEditorMount} onMount={handleEditorMount}
onChange={(value) => { onChange={(value) => {
if (value === activeFileContent) { if (value === activeFileContent) {
setTabs((prev) => setTabs((prev) =>
prev.map((tab) => prev.map((tab) =>
tab.id === activeFileId tab.id === activeFileId
? { ...tab, saved: true } ? { ...tab, saved: true }
: tab : tab
)
) )
} else { )
setTabs((prev) => } else {
prev.map((tab) => setTabs((prev) =>
tab.id === activeFileId prev.map((tab) =>
? { ...tab, saved: false } tab.id === activeFileId
: tab ? { ...tab, saved: false }
) : tab
) )
} )
}} }
options={{ }}
tabSize: 2, options={{
minimap: { tabSize: 2,
enabled: false, minimap: {
}, enabled: false,
padding: { },
bottom: 4, padding: {
top: 4, bottom: 4,
}, top: 4,
scrollBeyondLastLine: false, },
fixedOverflowWidgets: true, scrollBeyondLastLine: false,
fontFamily: "var(--font-geist-mono)", fixedOverflowWidgets: true,
}} fontFamily: "var(--font-geist-mono)",
theme="vs-dark" }}
value={activeFileContent} theme="vs-dark"
/> value={activeFileContent}
</> />
) : ( </>
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none"> ) : (
<Loader2 className="animate-spin w-6 h-6 mr-3" /> <div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
Waiting for Clerk to load... <Loader2 className="animate-spin w-6 h-6 mr-3" />
</div> Waiting for Clerk to load...
)} </div>
)}
</div> </div>
</ResizablePanel> </ResizablePanel>
<ResizableHandle /> <ResizableHandle />
@ -825,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
@ -849,4 +984,3 @@ export default function CodeEditor({
</> </>
) )
} }

View File

@ -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> */}

View File

@ -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",

View File

@ -42,13 +42,13 @@ export default function UserButton({ userData }: { userData: User }) {
<div className="py-1.5 px-2 w-full flex flex-col items-start text-sm"> <div className="py-1.5 px-2 w-full flex flex-col items-start text-sm">
<div className="flex items-center"> <div className="flex items-center">
<Sparkles className={`h-4 w-4 mr-2 text-indigo-500`} /> <Sparkles className={`h-4 w-4 mr-2 text-indigo-500`} />
AI Usage: {userData.generations}/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>

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 { 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
} }