Compare commits

..

9 Commits

Author SHA1 Message Date
a9c5db92ff fix: ignore certains files and folders from the file tree
- Created new config file for ignored paths in file system traversal
- Separated ignored folders and files into dedicated arrays
- Includes comprehensive ignore patterns for:
  - Package managers (node_modules, venv)
  - Build outputs and caches
  - Version control
  - IDE specific folders
  - Framework specific directories
  - System and config files
  - Lock files and compiled assets
2024-11-04 17:52:26 -05:00
2c9f130a37 chore: delete unused files 2024-11-04 17:23:16 -05:00
fac1404e14 feat: multi-file context, context tabs
- added context tabs
- added multifile context including file and image uploads to the context along with all the files from the project
- added file/image previews on input
- added code paste from the editor and file lines recognition
- added image paste from clipboard and preview
2024-11-04 14:21:13 -05:00
2317cf49e9 feat: enhance AI Chat with context management, file integration, image support, and improved code handling
- Added context tabs system for managing multiple types of context (files, code snippets, images)
   - Added preview functionality for context items
   - Added ability to expand/collapse context previews
   - Added file selection popup/dropdown
   - Added file search functionality
   - Added image upload button
   - Added image paste support
   - Added image preview in context tabs
   - Added automatic code detection on paste
   - Added line number tracking for code snippets
   - Added source file name preservation
   - Added line range display for code contexts
   - Added model selection dropdown (Claude 3.5 Sonnet/Claude 3)
   - Added Ctrl+Enter for sending with full context
   - Added Backspace to remove last context tab when input is empty
   - Added smart code detection on paste
2024-10-29 01:37:46 -04:00
24332794f1 chore: changing the links 2024-10-27 17:03:41 -04:00
a8b8a25e4c feat: add AI chat button to open it 2024-10-27 16:58:17 -04:00
88058ca710 fix: jsx.tolowercase error 2024-10-27 14:20:39 -04:00
7f6e2bf62d chore: removing unnecessary code 2024-10-27 14:17:31 -04:00
b48b08a274 chore: add posix to fix file not found errors 2024-10-27 14:17:08 -04:00
48 changed files with 10910 additions and 11069 deletions

View File

@ -11,6 +11,7 @@ For the latest updates, join our Discord server: [discord.gitwit.dev](https://di
Notes: Notes:
- Double check that whatever you change "SUPERDUPERSECRET" to, it's the same in all config files. - Double check that whatever you change "SUPERDUPERSECRET" to, it's the same in all config files.
- Right now we are loading project templates from a custom Cloudflare bucket which isn't covered in this guide, but that be updated/fixed very soon.
### 0. Requirements ### 0. Requirements
@ -196,48 +197,13 @@ DOKKU_KEY=
## Creating Custom Templates ## Creating Custom Templates
Anyone can contribute a custom template for integration in Sandbox. Since Sandbox is built on E2B, there is no limitation to what langauge or runtime a Sandbox can use. We're working on a process whereby anyone can contribute a custom template that others can use in the Sandbox environment. The process includes:
Currently there are four templates: - Creating a [custom E2B Sandbox](https://e2b.dev/docs/sandbox-template) including the template files and dependencies
- [jamesmurdza/dokku-reactjs-template](https://github.com/jamesmurdza/dokku-reactjs-template) - Creating a file to specify the run command (e.g. "npm run dev")
- [jamesmurdza/dokku-vanillajs-template](https://github.com/jamesmurdza/dokku-vanillajs-template) - Testing the template with Dokku for deployment
- [jamesmurdza/dokku-nextjs-template](https://github.com/jamesmurdza/dokku-nextjs-template)
- [jamesmurdza/dokku-streamlit-template](https://github.com/jamesmurdza/dokku-streamlit-template)
To create your own template, you can fork one of the above templates or start with a new blank repository. The template should have at least an `e2b.Dockerfile`, which is used by E2B to create the development environment. Optionally, a `Dockerfile` can be added which will be used to create the project build when it is deployed. Please reach out to us [on Discord](https://discord.gitwit.dev/) if you're interested in contributing.
To test the template, you must have an [E2B account](https://e2b.dev/) and the [E2B CLI tools](https://e2b.dev/docs/cli) installed. Then, in the Terminal, run:
```
e2b auth login
```
Then, navigate to your template directory and run the following command where **TEMPLATENAME** is the name of your template:
```
e2b template build -d e2b.Dockerfile -n TEMPLATENAME
```
Finally, to test your template run:
```
e2b sandbox spawn TEMPLATENAME
cd project
```
You will see a URL in the form of `https://xxxxxxxxxxxxxxxxxxx.e2b-staging.com`.
Now, run the command to start your development server.
To see the running server, visit the public url `https://<PORT>-xxxxxxxxxxxxxxxxxxx.e2b-staging.com`.
If you've done this and it works, let us know and we'll add your template to Sandbox! Please reach out to us [on Discord](https://discord.gitwit.dev/) with any questions or to submit your working template.
Note: In the future, we will add a way to specify the command triggered by the "Run" button (e.g. "npm run dev").
For more information, see:
- [Custom E2B Sandboxes](https://e2b.dev/docs/sandbox-template)
- [Dokku Builders](https://dokku.com/docs/deployment/builders/builder-management/)
## Contributing ## Contributing

View File

@ -1,7 +1,7 @@
{ {
"version": "5", "version": "5",
"dialect": "sqlite", "dialect": "sqlite",
"id": "afe10bff-362b-402c-bdb5-038341692f35", "id": "6570ba20-a672-400c-8147-7ba533784918",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"tables": { "tables": {
"sandbox": { "sandbox": {
@ -35,36 +35,12 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"createdAt": {
"name": "createdAt",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP"
},
"user_id": { "user_id": {
"name": "user_id", "name": "user_id",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
},
"likeCount": {
"name": "likeCount",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
},
"viewCount": {
"name": "viewCount",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
} }
}, },
"indexes": { "indexes": {
@ -117,43 +93,6 @@
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"avatarUrl": {
"name": "avatarUrl",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP"
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"generations": {
"name": "generations",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
} }
}, },
"indexes": { "indexes": {
@ -163,13 +102,6 @@
"id" "id"
], ],
"isUnique": true "isUnique": true
},
"user_username_unique": {
"name": "user_username_unique",
"columns": [
"username"
],
"isUnique": true
} }
}, },
"foreignKeys": {}, "foreignKeys": {},
@ -192,13 +124,6 @@
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
},
"sharedOn": {
"name": "sharedOn",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
} }
}, },
"indexes": {}, "indexes": {},

View File

@ -1,8 +1,8 @@
{ {
"version": "5", "version": "5",
"dialect": "sqlite", "dialect": "sqlite",
"id": "e570d5ac-700d-4e62-8a46-482b21ae1fe1", "id": "9f64104a-4954-40c0-8155-17755ea0a243",
"prevId": "afe10bff-362b-402c-bdb5-038341692f35", "prevId": "6570ba20-a672-400c-8147-7ba533784918",
"tables": { "tables": {
"sandbox": { "sandbox": {
"name": "sandbox", "name": "sandbox",
@ -35,36 +35,12 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"createdAt": {
"name": "createdAt",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP"
},
"user_id": { "user_id": {
"name": "user_id", "name": "user_id",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
},
"likeCount": {
"name": "likeCount",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
},
"viewCount": {
"name": "viewCount",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
} }
}, },
"indexes": { "indexes": {
@ -118,35 +94,12 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"username": { "image": {
"name": "username", "name": "image",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"avatarUrl": {
"name": "avatarUrl",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP"
},
"generations": {
"name": "generations",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
} }
}, },
"indexes": { "indexes": {
@ -156,13 +109,6 @@
"id" "id"
], ],
"isUnique": true "isUnique": true
},
"user_username_unique": {
"name": "user_username_unique",
"columns": [
"username"
],
"isUnique": true
} }
}, },
"foreignKeys": {}, "foreignKeys": {},
@ -185,13 +131,6 @@
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
},
"sharedOn": {
"name": "sharedOn",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
} }
}, },
"indexes": {}, "indexes": {},

View File

@ -0,0 +1,168 @@
{
"version": "5",
"dialect": "sqlite",
"id": "5baf10d6-7697-42ba-a11a-ee4c7bd7e91e",
"prevId": "9f64104a-4954-40c0-8155-17755ea0a243",
"tables": {
"sandbox": {
"name": "sandbox",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"visibility": {
"name": "visibility",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"sandbox_id_unique": {
"name": "sandbox_id_unique",
"columns": [
"id"
],
"isUnique": true
}
},
"foreignKeys": {
"sandbox_user_id_user_id_fk": {
"name": "sandbox_user_id_user_id_fk",
"tableFrom": "sandbox",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"user_id_unique": {
"name": "user_id_unique",
"columns": [
"id"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users_to_sandboxes": {
"name": "users_to_sandboxes",
"columns": {
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"sandboxId": {
"name": "sandboxId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"users_to_sandboxes_userId_user_id_fk": {
"name": "users_to_sandboxes_userId_user_id_fk",
"tableFrom": "users_to_sandboxes",
"tableTo": "user",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"users_to_sandboxes_sandboxId_sandbox_id_fk": {
"name": "users_to_sandboxes_sandboxId_sandbox_id_fk",
"tableFrom": "users_to_sandboxes",
"tableTo": "sandbox",
"columnsFrom": [
"sandboxId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}

View File

@ -0,0 +1,175 @@
{
"version": "5",
"dialect": "sqlite",
"id": "37e38b82-1494-4818-8c26-b9024cce3fa9",
"prevId": "5baf10d6-7697-42ba-a11a-ee4c7bd7e91e",
"tables": {
"sandbox": {
"name": "sandbox",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"visibility": {
"name": "visibility",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"sandbox_id_unique": {
"name": "sandbox_id_unique",
"columns": [
"id"
],
"isUnique": true
}
},
"foreignKeys": {
"sandbox_user_id_user_id_fk": {
"name": "sandbox_user_id_user_id_fk",
"tableFrom": "sandbox",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"user_id_unique": {
"name": "user_id_unique",
"columns": [
"id"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users_to_sandboxes": {
"name": "users_to_sandboxes",
"columns": {
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"sandboxId": {
"name": "sandboxId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"users_to_sandboxes_userId_user_id_fk": {
"name": "users_to_sandboxes_userId_user_id_fk",
"tableFrom": "users_to_sandboxes",
"tableTo": "user",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"users_to_sandboxes_sandboxId_sandbox_id_fk": {
"name": "users_to_sandboxes_sandboxId_sandbox_id_fk",
"tableFrom": "users_to_sandboxes",
"tableTo": "sandbox",
"columnsFrom": [
"sandboxId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}

View File

@ -5,29 +5,50 @@
{ {
"idx": 0, "idx": 0,
"version": "5", "version": "5",
"when": 1731288423588, "when": 1714540200800,
"tag": "0000_cuddly_patriot", "tag": "0000_big_rogue",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 1, "idx": 1,
"version": "5", "version": "5",
"when": 1731290863632, "when": 1714541190588,
"tag": "0001_opposite_newton_destine", "tag": "0001_empty_black_knight",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 2, "idx": 2,
"version": "5", "version": "5",
"when": 1731296235880, "when": 1714541209173,
"tag": "0002_rainy_fantastic_four", "tag": "0002_sour_ego",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 3, "idx": 3,
"version": "5", "version": "5",
"when": 1731297339306, "when": 1714541233589,
"tag": "0003_lying_snowbird", "tag": "0003_pale_overlord",
"breakpoints": true
},
{
"idx": 4,
"version": "5",
"when": 1714565073180,
"tag": "0004_cuddly_wolf_cub",
"breakpoints": true
},
{
"idx": 5,
"version": "5",
"when": 1714950365718,
"tag": "0005_last_the_twelve",
"breakpoints": true
},
{
"idx": 6,
"version": "5",
"when": 1716432225404,
"tag": "0006_lively_mattie_franklin",
"breakpoints": true "breakpoints": true
} }
] ]

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,7 @@
"drizzle-kit": "^0.20.17", "drizzle-kit": "^0.20.17",
"typescript": "^5.0.4", "typescript": "^5.0.4",
"vitest": "1.3.0", "vitest": "1.3.0",
"wrangler": "^3.86.0" "wrangler": "^3.0.0"
}, },
"dependencies": { "dependencies": {
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "^2.2.2",
@ -29,4 +29,4 @@
"itty-router-extras": "^0.4.6", "itty-router-extras": "^0.4.6",
"zod": "^3.22.4" "zod": "^3.22.4"
} }
} }

View File

@ -169,7 +169,6 @@ export default {
name: sb.name, name: sb.name,
type: sb.type, type: sb.type,
author: sb.author.name, author: sb.author.name,
authorAvatarUrl: sb.author.avatarUrl,
sharedOn: r.sharedOn, sharedOn: r.sharedOn,
} }
}) })
@ -283,26 +282,14 @@ export default {
id: z.string(), id: z.string(),
name: z.string(), name: z.string(),
email: z.string().email(), email: z.string().email(),
username: z.string(),
avatarUrl: z.string().optional(),
createdAt: z.string().optional(),
generations: z.number().optional(),
}) })
const body = await request.json() const body = await request.json()
const { id, name, email, username, avatarUrl, createdAt, generations } = userSchema.parse(body) const { id, name, email } = userSchema.parse(body)
const res = await db const res = await db
.insert(user) .insert(user)
.values({ .values({ id, name, email })
id,
name,
email,
username,
avatarUrl,
createdAt: createdAt ? new Date(createdAt) : new Date(),
generations,
})
.returning() .returning()
.get() .get()
return json({ res }) return json({ res })
@ -316,20 +303,6 @@ export default {
} else { } else {
return methodNotAllowed return methodNotAllowed
} }
} else if (path === "/api/user/check-username") {
if (method === "GET") {
const params = url.searchParams
const username = params.get("username")
if (!username) return invalidRequest
const exists = await db.query.user.findFirst({
where: (user, { eq }) => eq(user.username, username)
})
return json({ exists: !!exists })
}
return methodNotAllowed
} else return notFound } else return notFound
}, },
} }

View File

@ -1,7 +1,6 @@
import { createId } from "@paralleldrive/cuid2" import { createId } from "@paralleldrive/cuid2"
import { relations } from "drizzle-orm" import { relations } from "drizzle-orm"
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"
import { sql } from "drizzle-orm"
export const user = sqliteTable("user", { export const user = sqliteTable("user", {
id: text("id") id: text("id")
@ -10,10 +9,7 @@ export const user = sqliteTable("user", {
.unique(), .unique(),
name: text("name").notNull(), name: text("name").notNull(),
email: text("email").notNull(), email: text("email").notNull(),
username: text("username").notNull().unique(), image: text("image"),
avatarUrl: text("avatarUrl"),
createdAt: integer("createdAt", { mode: "timestamp_ms" })
.default(sql`CURRENT_TIMESTAMP`),
generations: integer("generations").default(0), generations: integer("generations").default(0),
}) })
@ -32,13 +28,10 @@ export const sandbox = sqliteTable("sandbox", {
name: text("name").notNull(), name: text("name").notNull(),
type: text("type").notNull(), type: text("type").notNull(),
visibility: text("visibility", { enum: ["public", "private"] }), visibility: text("visibility", { enum: ["public", "private"] }),
createdAt: integer("createdAt", { mode: "timestamp_ms" }) createdAt: integer("createdAt", { mode: "timestamp_ms" }),
.default(sql`CURRENT_TIMESTAMP`),
userId: text("user_id") userId: text("user_id")
.notNull() .notNull()
.references(() => user.id), .references(() => user.id),
likeCount: integer("likeCount").default(0),
viewCount: integer("viewCount").default(0),
}) })
export type Sandbox = typeof sandbox.$inferSelect export type Sandbox = typeof sandbox.$inferSelect

View File

@ -7,7 +7,6 @@ PORT=4000
WORKERS_KEY= WORKERS_KEY=
DATABASE_WORKER_URL= DATABASE_WORKER_URL=
STORAGE_WORKER_URL= STORAGE_WORKER_URL=
AI_WORKER_URL=
E2B_API_KEY= E2B_API_KEY=
DOKKU_HOST= DOKKU_HOST=
DOKKU_USERNAME= DOKKU_USERNAME=

View File

@ -140,12 +140,6 @@ export class FileManager {
}) })
await Promise.all(promises) await Promise.all(promises)
// Reload file list from the container to include template files
const result = await this.sandbox.commands.run(`find "${this.dirName}" -type f`); // List all files recursively
const localPaths = result.stdout.split('\n').filter(path => path); // Split the output into an array and filter out empty strings
const relativePaths = localPaths.map(filePath => path.posix.relative(this.dirName, filePath)); // Convert absolute paths to relative paths
this.files = generateFileStructure(relativePaths);
// Make the logged in user the owner of all project files // Make the logged in user the owner of all project files
this.fixPermissions() this.fixPermissions()
@ -354,9 +348,8 @@ export class FileManager {
// Get file content // Get file content
async getFile(fileId: string): Promise<string | undefined> { async getFile(fileId: string): Promise<string | undefined> {
const filePath = path.posix.join(this.dirName, fileId) const file = this.fileData.find((f) => f.id === fileId)
const fileContent = await this.sandbox.files.read(filePath) return file?.data
return fileContent
} }
// Get folder content // Get folder content

View File

@ -13,8 +13,9 @@ import {
} from "./ratelimit" } from "./ratelimit"
import { SecureGitClient } from "./SecureGitClient" import { SecureGitClient } from "./SecureGitClient"
import { TerminalManager } from "./TerminalManager" import { TerminalManager } from "./TerminalManager"
import { TFile, TFileData, TFolder } from "./types" import { TFile, TFolder } from "./types"
import { LockManager } from "./utils" import { LockManager } from "./utils"
const lockManager = new LockManager() const lockManager = new LockManager()
// Define a type for SocketHandler functions // Define a type for SocketHandler functions
@ -37,7 +38,6 @@ type ServerContext = {
export class Sandbox { export class Sandbox {
// Sandbox properties: // Sandbox properties:
sandboxId: string; sandboxId: string;
type: string;
fileManager: FileManager | null; fileManager: FileManager | null;
terminalManager: TerminalManager | null; terminalManager: TerminalManager | null;
container: E2BSandbox | null; container: E2BSandbox | null;
@ -46,10 +46,9 @@ export class Sandbox {
gitClient: SecureGitClient | null; gitClient: SecureGitClient | null;
aiWorker: AIWorker; aiWorker: AIWorker;
constructor(sandboxId: string, type: string, { aiWorker, dokkuClient, gitClient }: ServerContext) { constructor(sandboxId: string, { aiWorker, dokkuClient, gitClient }: ServerContext) {
// Sandbox properties: // Sandbox properties:
this.sandboxId = sandboxId; this.sandboxId = sandboxId;
this.type = type;
this.fileManager = null; this.fileManager = null;
this.terminalManager = null; this.terminalManager = null;
this.container = null; this.container = null;
@ -70,12 +69,8 @@ export class Sandbox {
console.log(`Found existing container ${this.sandboxId}`) console.log(`Found existing container ${this.sandboxId}`)
} else { } else {
console.log("Creating container", this.sandboxId) console.log("Creating container", this.sandboxId)
// Create a new container with a specified template and timeout // Create a new container with a specified timeout
const templateTypes = ["vanillajs", "reactjs", "nextjs", "streamlit"]; this.container = await E2BSandbox.create({
const template = templateTypes.includes(this.type)
? `gitwit-${this.type}`
: `base`;
this.container = await E2BSandbox.create(template, {
timeoutMs: CONTAINER_TIMEOUT, timeoutMs: CONTAINER_TIMEOUT,
}) })
} }
@ -223,23 +218,9 @@ export class Sandbox {
return this.aiWorker.generateCode(connection.userId, fileName, code, line, instructions) return this.aiWorker.generateCode(connection.userId, fileName, code, line, instructions)
} }
// Handle downloading files by download button
const handleDownloadFiles: SocketHandler = async () => {
if (!this.fileManager) throw Error("No file manager")
// Get all files with their data through fileManager
const files = this.fileManager.fileData.map((file: TFileData) => ({
path: file.id.startsWith('/') ? file.id.slice(1) : file.id,
content: file.data
}))
return { files }
}
return { return {
"heartbeat": handleHeartbeat, "heartbeat": handleHeartbeat,
"getFile": handleGetFile, "getFile": handleGetFile,
"downloadFiles": handleDownloadFiles,
"getFolder": handleGetFolder, "getFolder": handleGetFolder,
"saveFile": handleSaveFile, "saveFile": handleSaveFile,
"moveFile": handleMoveFile, "moveFile": handleMoveFile,

View File

@ -96,7 +96,6 @@ io.on("connection", async (socket) => {
userId: string userId: string
sandboxId: string sandboxId: string
isOwner: boolean isOwner: boolean
type: string
} }
// Register the connection // Register the connection
@ -112,7 +111,6 @@ io.on("connection", async (socket) => {
// Create or retrieve the sandbox manager for the given sandbox ID // Create or retrieve the sandbox manager for the given sandbox ID
const sandbox = sandboxes[data.sandboxId] ?? new Sandbox( const sandbox = sandboxes[data.sandboxId] ?? new Sandbox(
data.sandboxId, data.sandboxId,
data.type,
{ {
aiWorker, dokkuClient, gitClient, aiWorker, dokkuClient, gitClient,
} }

View File

@ -1,6 +1,6 @@
import { Socket } from "socket.io" import { Socket } from "socket.io"
import { z } from "zod" import { z } from "zod"
import { Sandbox, User } from "./types" import { User } from "./types"
// Middleware for socket authentication // Middleware for socket authentication
export const socketAuth = async (socket: Socket, next: Function) => { export const socketAuth = async (socket: Socket, next: Function) => {
@ -33,17 +33,6 @@ export const socketAuth = async (socket: Socket, next: Function) => {
) )
const dbUserJSON = (await dbUser.json()) as User const dbUserJSON = (await dbUser.json()) as User
// Fetch sandbox data from the database
const dbSandbox = await fetch(
`${process.env.DATABASE_WORKER_URL}/api/sandbox?id=${sandboxId}`,
{
headers: {
Authorization: `${process.env.WORKERS_KEY}`,
},
}
)
const dbSandboxJSON = (await dbSandbox.json()) as Sandbox
// Check if user data was retrieved successfully // Check if user data was retrieved successfully
if (!dbUserJSON) { if (!dbUserJSON) {
next(new Error("DB error.")) next(new Error("DB error."))
@ -67,7 +56,6 @@ export const socketAuth = async (socket: Socket, next: Function) => {
userId, userId,
sandboxId: sandboxId, sandboxId: sandboxId,
isOwner: sandbox !== undefined, isOwner: sandbox !== undefined,
type: dbSandboxJSON.type
} }
// Allow the connection // Allow the connection

View File

@ -12,7 +12,7 @@ export type User = {
export type Sandbox = { export type Sandbox = {
id: string id: string
name: string name: string
type: "reactjs" | "vanillajs" | "nextjs" | "streamlit" type: "react" | "node"
visibility: "public" | "private" visibility: "public" | "private"
createdAt: Date createdAt: Date
userId: string userId: string

File diff suppressed because it is too large Load Diff

View File

@ -11,13 +11,13 @@
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.1.0", "@cloudflare/vitest-pool-workers": "^0.1.0",
"@cloudflare/workers-types": "^4.20241106.0", "@cloudflare/workers-types": "^4.20240419.0",
"typescript": "^5.0.4", "typescript": "^5.0.4",
"vitest": "1.3.0", "vitest": "1.3.0",
"wrangler": "^3.86.0" "wrangler": "^3.0.0"
}, },
"dependencies": { "dependencies": {
"p-limit": "^6.1.0", "p-limit": "^6.1.0",
"zod": "^3.23.4" "zod": "^3.23.4"
} }
} }

View File

@ -1,4 +1,4 @@
import { ExecutionContext, R2Bucket, Headers as CFHeaders } from "@cloudflare/workers-types" import pLimit from "p-limit"
import { z } from "zod" import { z } from "zod"
export interface Env { export interface Env {
@ -76,13 +76,14 @@ export default {
if (obj === null) { if (obj === null) {
return new Response(`${fileId} not found`, { status: 404 }) return new Response(`${fileId} not found`, { status: 404 })
} }
const headers = new Headers() as unknown as CFHeaders const headers = new Headers()
headers.set("etag", obj.httpEtag) headers.set("etag", obj.httpEtag)
obj.writeHttpMetadata(headers) obj.writeHttpMetadata(headers)
const text = await obj.text() const text = await obj.text()
return new Response(text, { return new Response(text, {
headers: Object.fromEntries(headers.entries()), headers,
}) })
} else return invalidRequest } else return invalidRequest
} else if (method === "POST") { } else if (method === "POST") {
@ -135,7 +136,33 @@ export default {
return success return success
} else if (path === "/api/init" && method === "POST") { } else if (path === "/api/init" && method === "POST") {
// This API path no longer does anything, because template files are stored in E2B sandbox templates. const initSchema = z.object({
sandboxId: z.string(),
type: z.string(),
})
const body = await request.json()
const { sandboxId, type } = initSchema.parse(body)
console.log(`Copying template: ${type}`)
// List all objects under the directory
const { objects } = await env.Templates.list({ prefix: type })
// 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 return success
} else { } else {
return notFound return notFound

View File

@ -34,7 +34,7 @@
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
"types": [ "types": [
"@cloudflare/workers-types" "@cloudflare/workers-types/2023-07-01"
] /* Specify type package names to be included without being referenced in a source file. */, ] /* Specify type package names to be included without being referenced in a source file. */,
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
"resolveJsonModule": true /* Enable importing .json files */, "resolveJsonModule": true /* Enable importing .json files */,

View File

@ -8,10 +8,8 @@ NEXT_PUBLIC_APP_URL=http://localhost:3000
# Set WORKER_URLs after deploying the workers. # Set WORKER_URLs after deploying the workers.
# Set NEXT_PUBLIC_WORKERS_KEY to be the same as KEY in /backend/storage/wrangler.toml. # Set NEXT_PUBLIC_WORKERS_KEY to be the same as KEY in /backend/storage/wrangler.toml.
# These URLs should begin with https:// in production
NEXT_PUBLIC_DATABASE_WORKER_URL= NEXT_PUBLIC_DATABASE_WORKER_URL=
NEXT_PUBLIC_STORAGE_WORKER_URL= NEXT_PUBLIC_STORAGE_WORKER_URL=
NEXT_PUBLIC_AI_WORKER_URL=
NEXT_PUBLIC_WORKERS_KEY= NEXT_PUBLIC_WORKERS_KEY=
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in

View File

@ -1,4 +1,4 @@
// import { Room } from "@/components/editor/live/room" import { Room } from "@/components/editor/live/room"
import Loading from "@/components/editor/loading" import Loading from "@/components/editor/loading"
import Navbar from "@/components/editor/navbar" import Navbar from "@/components/editor/navbar"
import { TerminalProvider } from "@/context/TerminalContext" import { TerminalProvider } from "@/context/TerminalContext"
@ -51,7 +51,7 @@ const getSharedUsers = async (usersToSandboxes: UsersToSandboxes[]) => {
} }
) )
const userData: User = await userRes.json() const userData: User = await userRes.json()
return { id: userData.id, name: userData.name, avatarUrl: userData.avatarUrl } return { id: userData.id, name: userData.name }
}) })
) )
@ -89,18 +89,18 @@ export default async function CodePage({ params }: { params: { id: string } }) {
return ( return (
<> <>
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background"> <div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
{/* <Room id={sandboxId}> */} <Room id={sandboxId}>
<TerminalProvider> <TerminalProvider>
<Navbar <Navbar
userData={userData} userData={userData}
sandboxData={sandboxData} sandboxData={sandboxData}
shared={shared as { id: string; name: string; avatarUrl: string }[]} shared={shared}
/> />
<div className="w-screen flex grow"> <div className="w-screen flex grow">
<CodeEditor userData={userData} sandboxData={sandboxData} /> <CodeEditor userData={userData} sandboxData={sandboxData} />
</div> </div>
</TerminalProvider> </TerminalProvider>
{/* </Room> */} </Room>
</div> </div>
</> </>
) )

View File

@ -35,7 +35,6 @@ export default async function DashboardPage() {
type: "react" | "node" type: "react" | "node"
author: string author: string
sharedOn: Date sharedOn: Date
authorAvatarUrl: string
}[] }[]
return ( return (

View File

@ -1,7 +1,6 @@
import { User } from "@/lib/types" import { User } from "@/lib/types"
import { currentUser } from "@clerk/nextjs" import { currentUser } from "@clerk/nextjs"
import { redirect } from "next/navigation" import { redirect } from "next/navigation"
import { generateUniqueUsername } from "@/lib/username-generator";
export default async function AppAuthLayout({ export default async function AppAuthLayout({
children, children,
@ -25,25 +24,6 @@ export default async function AppAuthLayout({
const dbUserJSON = (await dbUser.json()) as User const dbUserJSON = (await dbUser.json()) as User
if (!dbUserJSON.id) { if (!dbUserJSON.id) {
// Try to get GitHub username if available
const githubUsername = user.externalAccounts.find(
account => account.provider === "github"
)?.username;
const username = githubUsername || await generateUniqueUsername(async (username) => {
// Check if username exists in database
const userCheck = await fetch(
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user/check-username?username=${username}`,
{
headers: {
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
},
}
)
const exists = await userCheck.json()
return exists.exists
});
const res = await fetch( const res = await fetch(
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user`, `${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user`,
{ {
@ -56,20 +36,9 @@ export default async function AppAuthLayout({
id: user.id, id: user.id,
name: user.firstName + " " + user.lastName, name: user.firstName + " " + user.lastName,
email: user.emailAddresses[0].emailAddress, email: user.emailAddresses[0].emailAddress,
username: username,
avatarUrl: user.imageUrl || null,
createdAt: new Date().toISOString(),
}), }),
} }
) )
if (!res.ok) {
const error = await res.text();
console.error("Failed to create user:", error);
} else {
const data = await res.json();
console.log("User created successfully:", data);
}
} }
return <>{children}</> return <>{children}</>

View File

@ -1,61 +1,57 @@
// import { colors } from "@/lib/colors" import { colors } from "@/lib/colors"
// import { User } from "@/lib/types" import { User } from "@/lib/types"
import { currentUser } from "@clerk/nextjs" import { currentUser } from "@clerk/nextjs"
// import { Liveblocks } from "@liveblocks/node" import { Liveblocks } from "@liveblocks/node"
import { NextRequest } from "next/server" import { NextRequest } from "next/server"
// const API_KEY = process.env.LIVEBLOCKS_SECRET_KEY! const API_KEY = process.env.LIVEBLOCKS_SECRET_KEY!
// const liveblocks = new Liveblocks({ const liveblocks = new Liveblocks({
// secret: API_KEY!, secret: API_KEY!,
// }) })
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
// Temporarily return unauthorized while Liveblocks is disabled const clerkUser = await currentUser()
return new Response("Liveblocks collaboration temporarily disabled", { status: 503 })
// Original implementation commented out: if (!clerkUser) {
// const clerkUser = await currentUser() return new Response("Unauthorized", { status: 401 })
// }
// if (!clerkUser) {
// return new Response("Unauthorized", { status: 401 }) const res = await fetch(
// } `${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user?id=${clerkUser.id}`,
// {
// const res = await fetch( headers: {
// `${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user?id=${clerkUser.id}`, Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
// { },
// headers: { }
// Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`, )
// }, const user = (await res.json()) as User
// }
// ) const colorNames = Object.keys(colors)
// const user = (await res.json()) as User const randomColor = colorNames[
// Math.floor(Math.random() * colorNames.length)
// const colorNames = Object.keys(colors) ] as keyof typeof colors
// const randomColor = colorNames[ const code = colors[randomColor]
// Math.floor(Math.random() * colorNames.length)
// ] as keyof typeof colors // Create a session for the current user
// const code = colors[randomColor] // userInfo is made available in Liveblocks presence hooks, e.g. useOthers
// const session = liveblocks.prepareSession(user.id, {
// // Create a session for the current user userInfo: {
// // userInfo is made available in Liveblocks presence hooks, e.g. useOthers name: user.name,
// const session = liveblocks.prepareSession(user.id, { email: user.email,
// userInfo: { color: randomColor,
// name: user.name, },
// email: user.email, })
// color: randomColor,
// }, // Give the user access to the room
// }) user.sandbox.forEach((sandbox) => {
// session.allow(`${sandbox.id}`, session.FULL_ACCESS)
// // Give the user access to the room })
// user.sandbox.forEach((sandbox) => { user.usersToSandboxes.forEach((userToSandbox) => {
// session.allow(`${sandbox.id}`, session.FULL_ACCESS) session.allow(`${userToSandbox.sandboxId}`, session.FULL_ACCESS)
// }) })
// user.usersToSandboxes.forEach((userToSandbox) => {
// session.allow(`${userToSandbox.sandboxId}`, session.FULL_ACCESS) // Authorize the user and return the result
// }) const { body, status } = await session.authorize()
// return new Response(body, { status })
// // Authorize the user and return the result
// const { body, status } = await session.authorize()
// return new Response(body, { status })
} }

View File

@ -18,38 +18,11 @@ export default function AboutModal({
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Help & Support</DialogTitle> <DialogTitle>About this project</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="text-sm text-muted-foreground">
{/* <div className="text-sm text-muted-foreground"> Sandbox is an open-source cloud-based code editing environment with
Sandbox is an open-source cloud-based code editing environment with custom AI code autocompletion and real-time collaboration.
custom AI code autocompletion and real-time collaboration.
</div> */}
<div className="text-sm text-muted-foreground">
Get help and support through our Discord community or by creating issues on GitHub:
</div>
<div className="space-y-2">
<div className="text-sm">
<a
href="https://discord.gitwit.dev/"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Join our Discord community
</a>
</div>
<div className="text-sm">
<a
href="https://github.com/jamesmurdza/sandbox/issues"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Report issues on GitHub
</a>
</div>
</div>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -25,7 +25,6 @@ export default function Dashboard({
type: "react" | "node" type: "react" | "node"
author: string author: string
sharedOn: Date sharedOn: Date
authorAvatarUrl?: string
}[] }[]
}) { }) {
const [screen, setScreen] = useState<TScreen>("projects") const [screen, setScreen] = useState<TScreen>("projects")
@ -78,14 +77,14 @@ export default function Dashboard({
<FolderDot className="w-4 h-4 mr-2" /> <FolderDot className="w-4 h-4 mr-2" />
My Projects My Projects
</Button> </Button>
{/* <Button <Button
variant="ghost" variant="ghost"
onClick={() => setScreen("shared")} onClick={() => setScreen("shared")}
className={activeScreen("shared")} className={activeScreen("shared")}
> >
<Users className="w-4 h-4 mr-2" /> <Users className="w-4 h-4 mr-2" />
Shared With Me Shared With Me
</Button> */} </Button>
{/* <Button {/* <Button
variant="ghost" variant="ghost"
onClick={() => setScreen("settings")} onClick={() => setScreen("settings")}
@ -111,7 +110,7 @@ export default function Dashboard({
className="justify-start font-normal text-muted-foreground" className="justify-start font-normal text-muted-foreground"
> >
<HelpCircle className="w-4 h-4 mr-2" /> <HelpCircle className="w-4 h-4 mr-2" />
Help About
</Button> </Button>
</div> </div>
</div> </div>
@ -122,12 +121,7 @@ export default function Dashboard({
) : null} ) : null}
</> </>
) : screen === "shared" ? ( ) : screen === "shared" ? (
<DashboardSharedWithMe <DashboardSharedWithMe shared={shared} />
shared={shared.map((item) => ({
...item,
authorAvatarUrl: item.authorAvatarUrl || "",
}))}
/>
) : screen === "settings" ? null : null} ) : screen === "settings" ? null : null}
</div> </div>
</> </>

View File

@ -11,7 +11,6 @@ import Image from "next/image"
import Link from "next/link" import Link from "next/link"
import Avatar from "../ui/avatar" import Avatar from "../ui/avatar"
import Button from "../ui/customButton" import Button from "../ui/customButton"
import { projectTemplates } from "@/lib/data"
export default function DashboardSharedWithMe({ export default function DashboardSharedWithMe({
shared, shared,
@ -19,9 +18,8 @@ export default function DashboardSharedWithMe({
shared: { shared: {
id: string id: string
name: string name: string
type: string type: "react" | "node"
author: string author: string
authorAvatarUrl: string
sharedOn: Date sharedOn: Date
}[] }[]
}) { }) {
@ -47,7 +45,9 @@ export default function DashboardSharedWithMe({
<Image <Image
alt="" alt=""
src={ src={
projectTemplates.find((p) => p.id === sandbox.type)?.icon ?? "/project-icons/node.svg" sandbox.type === "react"
? "/project-icons/react.svg"
: "/project-icons/node.svg"
} }
width={20} width={20}
height={20} height={20}
@ -58,11 +58,7 @@ export default function DashboardSharedWithMe({
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex items-center"> <div className="flex items-center">
<Avatar <Avatar name={sandbox.author} className="mr-2" />
name={sandbox.author}
avatarUrl={sandbox.authorAvatarUrl}
className="mr-2"
/>
{sandbox.author} {sandbox.author}
</div> </div>
</TableCell> </TableCell>

View File

@ -7,11 +7,11 @@ import * as monaco from "monaco-editor"
import { useCallback, useEffect, useRef, useState } from "react" import { useCallback, useEffect, useRef, useState } from "react"
import { toast } from "sonner" import { toast } from "sonner"
// import { TypedLiveblocksProvider, useRoom, useSelf } from "@/liveblocks.config" import { TypedLiveblocksProvider, useRoom, useSelf } from "@/liveblocks.config"
// import LiveblocksProvider from "@liveblocks/yjs" import LiveblocksProvider from "@liveblocks/yjs"
// import { MonacoBinding } from "y-monaco" import { MonacoBinding } from "y-monaco"
// import { Awareness } from "y-protocols/awareness" import { Awareness } from "y-protocols/awareness"
// import * as Y from "yjs" import * as Y from "yjs"
import { import {
ResizableHandle, ResizableHandle,
@ -46,7 +46,7 @@ import { Button } from "../ui/button"
import Tab from "../ui/tab" import Tab from "../ui/tab"
import AIChat from "./AIChat" import AIChat from "./AIChat"
import GenerateInput from "./generate" import GenerateInput from "./generate"
// import { Cursors } from "./live/cursors" import { Cursors } from "./live/cursors"
import DisableAccessModal from "./live/disableModal" import DisableAccessModal from "./live/disableModal"
import Loading from "./loading" import Loading from "./loading"
import PreviewWindow from "./preview" import PreviewWindow from "./preview"
@ -147,20 +147,20 @@ export default function CodeEditor({
const isOwner = sandboxData.userId === userData.id const isOwner = sandboxData.userId === userData.id
const clerk = useClerk() const clerk = useClerk()
// // Liveblocks hooks // Liveblocks hooks
// const room = useRoom() const room = useRoom()
// const [provider, setProvider] = useState<TypedLiveblocksProvider>() const [provider, setProvider] = useState<TypedLiveblocksProvider>()
// const userInfo = useSelf((me) => me.info) const userInfo = useSelf((me) => me.info)
// // 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)
@ -221,6 +221,7 @@ export default function CodeEditor({
let mergedConfig: any = { compilerOptions: {} } let mergedConfig: any = { compilerOptions: {} }
for (const file of tsconfigFiles) { for (const file of tsconfigFiles) {
const containerId = file.id.split("/").slice(0, 2).join("/")
const content = await fetchFileContent(file.id) const content = await fetchFileContent(file.id)
try { try {
@ -230,7 +231,8 @@ export default function CodeEditor({
if (tsConfig.references) { if (tsConfig.references) {
for (const ref of tsConfig.references) { for (const ref of tsConfig.references) {
const path = ref.path.replace("./", "") const path = ref.path.replace("./", "")
const refContent = await fetchFileContent(path) const fileId = `${containerId}/${path}`
const refContent = await fetchFileContent(fileId)
const referenceTsConfig = JSON.parse(refContent) const referenceTsConfig = JSON.parse(refContent)
// Merge configurations // Merge configurations
@ -541,6 +543,8 @@ export default function CodeEditor({
tab.id === activeFileId ? { ...tab, saved: true } : tab tab.id === activeFileId ? { ...tab, saved: true } : tab
) )
) )
console.log(`Saving file...${activeFileId}`)
console.log(`Saving file...${content}`)
socket?.emit("saveFile", { fileId: activeFileId, body: content }) socket?.emit("saveFile", { fileId: activeFileId, body: content })
} }
}, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000), }, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000),
@ -571,82 +575,82 @@ export default function CodeEditor({
} }
}, [activeFileId, tabs, debouncedSaveData, setIsAIChatOpen, editorRef]) }, [activeFileId, tabs, debouncedSaveData, setIsAIChatOpen, editorRef])
// // Liveblocks live collaboration setup effect // Liveblocks live collaboration setup effect
// useEffect(() => { useEffect(() => {
// const tab = tabs.find((t) => t.id === activeFileId) const tab = tabs.find((t) => t.id === activeFileId)
// const model = editorRef?.getModel() const model = editorRef?.getModel()
// 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) => {
// if (isSynced) { if (isSynced) {
// const text = yText.toString() const text = yText.toString()
// if (text === "") { if (text === "") {
// if (activeFileContent) { if (activeFileContent) {
// yText.insert(0, activeFileContent) yText.insert(0, activeFileContent)
// } else { } else {
// setTimeout(() => { setTimeout(() => {
// yText.insert(0, editorRef.getValue()) yText.insert(0, editorRef.getValue())
// }, 0) }, 0)
// } }
// } }
// } }
// } }
// 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(
// providerData.yText, providerData.yText,
// 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(() => {
// return () => { return () => {
// // 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(() => {
@ -1088,9 +1092,9 @@ export default function CodeEditor({
) : // 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}

View File

@ -1,34 +0,0 @@
import JSZip from 'jszip'
import { useSocket } from "@/context/SocketContext"
import { Button } from "@/components/ui/button"
import { Download } from "lucide-react"
export default function DownloadButton({ name }: { name: string }) {
const { socket } = useSocket()
const handleDownload = async () => {
socket?.emit("downloadFiles", {}, async (response: {files: {path: string, content: string}[]}) => {
const zip = new JSZip()
response.files.forEach(file => {
zip.file(file.path, file.content)
})
const blob = await zip.generateAsync({type: "blob"})
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${name}.zip`
a.click()
window.URL.revokeObjectURL(url)
})
}
return (
<Button variant="outline" onClick={handleDownload}>
<Download className="w-4 h-4 mr-2" />
Download
</Button>
)
}

View File

@ -9,12 +9,11 @@ import { Pencil, Users } from "lucide-react"
import Image from "next/image" import Image from "next/image"
import Link from "next/link" import Link from "next/link"
import { useState } from "react" import { useState } from "react"
// import { Avatars } from "../live/avatars" import { Avatars } from "../live/avatars"
import DeployButtonModal from "./deploy" import DeployButtonModal from "./deploy"
import EditSandboxModal from "./edit" import EditSandboxModal from "./edit"
import RunButtonModal from "./run" import RunButtonModal from "./run"
import ShareSandboxModal from "./share" import ShareSandboxModal from "./share"
import DownloadButton from "./downloadButton"
export default function Navbar({ export default function Navbar({
userData, userData,
@ -23,7 +22,7 @@ export default function Navbar({
}: { }: {
userData: User userData: User
sandboxData: Sandbox sandboxData: Sandbox
shared: { id: string; name: string; avatarUrl: string }[] shared: { id: string; name: string }[]
}) { }) {
const [isEditOpen, setIsEditOpen] = useState(false) const [isEditOpen, setIsEditOpen] = useState(false)
const [isShareOpen, setIsShareOpen] = useState(false) const [isShareOpen, setIsShareOpen] = useState(false)
@ -70,16 +69,16 @@ export default function Navbar({
sandboxData={sandboxData} sandboxData={sandboxData}
/> />
<div className="flex items-center h-full space-x-4"> <div className="flex items-center h-full space-x-4">
{/* <Avatars /> */} <Avatars />
{isOwner ? ( {isOwner ? (
<> <>
<DeployButtonModal data={sandboxData} userData={userData} /> <DeployButtonModal data={sandboxData} userData={userData} />
{/* <Button variant="outline" onClick={() => setIsShareOpen(true)}> <Button variant="outline" onClick={() => setIsShareOpen(true)}>
<Users className="w-4 h-4 mr-2" /> <Users className="w-4 h-4 mr-2" />
Share Share
</Button> */} </Button>
<DownloadButton name={sandboxData.name} /></> </>
) : null} ) : null}
<ThemeSwitcher /> <ThemeSwitcher />
<UserButton userData={userData} /> <UserButton userData={userData} />

View File

@ -44,8 +44,8 @@ export default function RunButtonModal({
} else if (!isRunning && terminals.length < 4) { } else if (!isRunning && terminals.length < 4) {
const command = const command =
sandboxData.type === "streamlit" sandboxData.type === "streamlit"
? "./venv/bin/streamlit run main.py --server.runOnSave true" ? "pip install -r requirements.txt && streamlit run main.py --server.runOnSave true"
: "npm run dev" : "yarn install && yarn dev"
try { try {
// Create a new terminal with the appropriate command // Create a new terminal with the appropriate command

View File

@ -43,7 +43,6 @@ export default function ShareSandboxModal({
shared: { shared: {
id: string id: string
name: string name: string
avatarUrl: string
}[] }[]
}) { }) {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@ -143,11 +142,7 @@ export default function ShareSandboxModal({
</DialogHeader> </DialogHeader>
<div className="space-y-2"> <div className="space-y-2">
{shared.map((user) => ( {shared.map((user) => (
<SharedUser <SharedUser key={user.id} user={user} sandboxId={data.id} />
key={user.id}
user={user}
sandboxId={data.id}
/>
))} ))}
</div> </div>
</div> </div>

View File

@ -10,7 +10,7 @@ export default function SharedUser({
user, user,
sandboxId, sandboxId,
}: { }: {
user: { id: string; name: string; avatarUrl: string } user: { id: string; name: string }
sandboxId: string sandboxId: string
}) { }) {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@ -24,7 +24,7 @@ export default function SharedUser({
return ( return (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center"> <div className="flex items-center">
<Avatar name={user.name} avatarUrl={user.avatarUrl} className="mr-2" /> <Avatar name={user.name} className="mr-2" />
{user.name} {user.name}
</div> </div>
<Button <Button

View File

@ -45,14 +45,9 @@ export default function Landing() {
<h1 className="text-2xl font-medium text-center mt-16"> <h1 className="text-2xl font-medium text-center mt-16">
A Collaborative + AI-Powered Code Environment A Collaborative + AI-Powered Code Environment
</h1> </h1>
{/* <p className="text-muted-foreground mt-4 text-center "> <p className="text-muted-foreground mt-4 text-center ">
Sandbox is an open-source cloud-based code editing environment with Sandbox is an open-source cloud-based code editing environment with
custom AI code autocompletion and real-time collaboration. custom AI code autocompletion and real-time collaboration.
</p> */}
<p className="text-muted-foreground mt-4 text-center ">
A cloud-based code editor featuring real-time collaboration,
intelligent code autocompletion, and an AI assistant to help you code
faster and smarter.
</p> </p>
<div className="mt-8 flex space-x-4"> <div className="mt-8 flex space-x-4">
<Link href="/sign-up"> <Link href="/sign-up">

View File

@ -1,59 +0,0 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

View File

@ -1,42 +1,23 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import Image from "next/image"
export default function Avatar({ export default function Avatar({
name, name,
avatarUrl,
className, className,
}: { }: {
name: string name: string
avatarUrl?: string | null
className?: string className?: string
}) { }) {
// Generate initials from name if no avatarUrl is provided
const initials = name
? name
.split(" ")
.slice(0, 2)
.map((letter) => letter[0].toUpperCase())
.join("")
: "?"
return ( return (
<div <div
className={cn( className={cn(
className, className,
"w-9 h-9 font-mono rounded-full overflow-hidden bg-gradient-to-t from-neutral-800 to-neutral-600 flex items-center justify-center text-sm font-medium" "w-5 h-5 font-mono rounded-full overflow-hidden bg-gradient-to-t from-neutral-800 to-neutral-600 flex items-center justify-center text-[0.5rem] font-medium"
)} )}
> >
{avatarUrl ? ( {name
<Image .split(" ")
src={avatarUrl} .slice(0, 2)
alt={name || "User"} .map((letter) => letter[0].toUpperCase())}
width={20}
height={20}
className="w-full h-full object-cover"
/>
) : (
initials
)}
</div> </div>
) )
} }

View File

@ -1,28 +0,0 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }

View File

@ -1,32 +0,0 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</TooltipPrimitive.Portal>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@ -11,7 +11,6 @@ import { User } from "@/lib/types"
import { useClerk } from "@clerk/nextjs" import { useClerk } from "@clerk/nextjs"
import { LogOut, Sparkles } from "lucide-react" import { LogOut, Sparkles } from "lucide-react"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import Avatar from "./avatar"
export default function UserButton({ userData }: { userData: User }) { export default function UserButton({ userData }: { userData: User }) {
if (!userData) return null if (!userData) return null
@ -22,7 +21,13 @@ export default function UserButton({ userData }: { userData: User }) {
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger> <DropdownMenuTrigger>
<Avatar name={userData.name} avatarUrl={userData.avatarUrl} /> <div className="w-9 h-9 font-mono rounded-full overflow-hidden bg-gradient-to-t from-neutral-800 to-neutral-600 flex items-center justify-center text-sm font-medium">
{userData.name &&
userData.name
.split(" ")
.slice(0, 2)
.map((name) => name[0].toUpperCase())}
</div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="w-48" align="end"> <DropdownMenuContent className="w-48" align="end">
<div className="py-1.5 px-2 w-full"> <div className="py-1.5 px-2 w-full">

View File

@ -16,7 +16,7 @@ export const projectTemplates: {
id: "vanillajs", id: "vanillajs",
name: "HTML/JS", name: "HTML/JS",
icon: "/project-icons/more.svg", icon: "/project-icons/more.svg",
description: "A simple HTML/JS project for building web apps", description: "More coming soon, feel free to contribute on GitHub",
disabled: false, disabled: false,
}, },
{ {

View File

@ -4,9 +4,6 @@ export type User = {
id: string id: string
name: string name: string
email: string email: string
username: string
avatarUrl: string | null
createdAt: Date
generations: number generations: number
sandbox: Sandbox[] sandbox: Sandbox[]
usersToSandboxes: UsersToSandboxes[] usersToSandboxes: UsersToSandboxes[]

View File

@ -1,82 +0,0 @@
// Constants for username generation
const WORDS = {
adjectives: [
"azure", "crimson", "golden", "silver", "violet", "emerald", "cobalt", "amber", "coral", "jade",
"cyber", "digital", "quantum", "neural", "binary", "cosmic", "stellar", "atomic", "crypto", "nano",
"swift", "brave", "clever", "wise", "noble", "rapid", "bright", "sharp", "keen", "bold",
"dynamic", "epic", "mega", "ultra", "hyper", "super", "prime", "elite", "alpha", "omega",
"pixel", "vector", "sonic", "laser", "matrix", "nexus", "proxy", "cloud", "data", "tech",
],
nouns: [
"coder", "hacker", "dev", "ninja", "guru", "wizard", "admin", "mod", "chief", "boss",
"wolf", "eagle", "phoenix", "dragon", "tiger", "falcon", "shark", "lion", "hawk", "bear",
"byte", "bit", "node", "stack", "cache", "chip", "core", "net", "web", "app",
"star", "nova", "pulsar", "comet", "nebula", "quasar", "cosmos", "orbit", "astro", "solar",
"mind", "soul", "spark", "pulse", "force", "power", "wave", "storm", "flash", "surge",
],
prefixes: [
"the", "mr", "ms", "dr", "pro", "master", "lord", "captain", "chief", "agent",
],
} as const;
// Helper function to get random element from array
const getRandomElement = <T>(array: readonly T[]): T => {
return array[Math.floor(Math.random() * array.length)];
};
// Username pattern generators
const usernamePatterns = {
basic: (): string => {
const adjective = getRandomElement(WORDS.adjectives);
const noun = getRandomElement(WORDS.nouns);
const number = Math.floor(Math.random() * 10000);
return `${adjective}${noun}${number}`;
},
prefixed: (): string => {
const prefix = getRandomElement(WORDS.prefixes);
const noun = getRandomElement(WORDS.nouns);
const number = Math.floor(Math.random() * 100);
return `${prefix}${noun}${number}`;
},
doubleAdjective: (): string => {
const adj1 = getRandomElement(WORDS.adjectives);
const adj2 = getRandomElement(WORDS.adjectives);
const noun = getRandomElement(WORDS.nouns);
return `${adj1}${adj2}${noun}`;
},
doubleNoun: (): string => {
const noun1 = getRandomElement(WORDS.nouns);
const noun2 = getRandomElement(WORDS.nouns);
const number = Math.floor(Math.random() * 100);
return `${noun1}${number}${noun2}`;
},
};
export function generateUsername(): string {
const patterns = Object.values(usernamePatterns);
const selectedPattern = getRandomElement(patterns);
return selectedPattern();
}
export async function generateUniqueUsername(
checkExists: (username: string) => Promise<boolean>
): Promise<string> {
const MAX_ATTEMPTS = 10;
let attempts = 0;
let username = generateUsername();
while (await checkExists(username) && attempts < MAX_ATTEMPTS) {
username = generateUsername();
attempts++;
}
if (attempts >= MAX_ATTEMPTS) {
// Add a large random number to ensure uniqueness
username = generateUsername() + Math.floor(Math.random() * 1000000);
}
return username;
}

View File

@ -5,12 +5,6 @@ const nextConfig = {
{ {
hostname: "cdn.simpleicons.org", hostname: "cdn.simpleicons.org",
}, },
{
hostname: "img.clerk.com",
},
{
hostname: "images.clerk.dev",
},
], ],
}, },
} }

File diff suppressed because it is too large Load Diff

View File

@ -27,11 +27,9 @@
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-select": "^2.0.0", "@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tooltip": "^1.1.3",
"@react-three/fiber": "^8.16.6", "@react-three/fiber": "^8.16.6",
"@uiw/codemirror-theme-vscode": "^4.23.5", "@uiw/codemirror-theme-vscode": "^4.23.5",
"@uiw/react-codemirror": "^4.23.5", "@uiw/react-codemirror": "^4.23.5",
@ -46,7 +44,6 @@
"framer-motion": "^11.2.3", "framer-motion": "^11.2.3",
"fs": "^0.0.1-security", "fs": "^0.0.1-security",
"geist": "^1.3.0", "geist": "^1.3.0",
"jszip": "^3.10.1",
"lucide-react": "^0.365.0", "lucide-react": "^0.365.0",
"monaco-themes": "^0.4.4", "monaco-themes": "^0.4.4",
"next": "14.1.3", "next": "14.1.3",
@ -59,7 +56,6 @@
"react-resizable-panels": "^2.0.16", "react-resizable-panels": "^2.0.16",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"shadcn": "^2.1.6",
"socket.io-client": "^4.7.5", "socket.io-client": "^4.7.5",
"sonner": "^1.4.41", "sonner": "^1.4.41",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.3.0",

2299
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

8
package.json Normal file
View File

@ -0,0 +1,8 @@
{
"dependencies": {
"@radix-ui/react-popover": "^1.1.1"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.15"
}
}