Compare commits
50 Commits
feature/ap
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
bcbbe5abfd | ||
|
15426cac57 | ||
|
3b8c9e12e4 | ||
|
1f4e70788e | ||
|
b0d444980a | ||
|
cfa9485e97 | ||
|
960a104011 | ||
|
59880fcd6e | ||
|
92584a3ea6 | ||
|
cfc0ae6cf4 | ||
|
e4f4bcfdcc | ||
|
108fa4ff1f | ||
|
103faf1d09 | ||
|
2c7527055f | ||
|
bde591006e | ||
|
94ffbaf60f | ||
|
e62849ab96 | ||
|
250fb45cb9 | ||
|
49d0690081 | ||
|
f38bf79e54 | ||
|
1eb17a0ac5 | ||
|
996eb4339e | ||
|
5faafd477b | ||
|
b88feee86e | ||
|
ceeb1fbce3 | ||
|
024e30bd99 | ||
|
947f0c6f0d | ||
|
f800008487 | ||
|
1d21d55437 | ||
|
274c945fac | ||
|
a41cdff55f | ||
|
182d527416 | ||
|
f3f91ea16f | ||
|
e8590703f8 | ||
|
093a4e9758 | ||
|
6a60f4d286 | ||
|
260110724e | ||
|
ee51ae7a33 | ||
|
0d0eed34b2 | ||
|
4e1c5cac27 | ||
|
0ae89341d2 | ||
|
1630a5a9cd | ||
|
ffdcdef56c | ||
|
6612692d98 | ||
|
534b148b86 | ||
|
e384607d24 | ||
|
e7d9989931 | ||
|
42305d67b9 | ||
|
06dadf3a0b | ||
|
ba7a1dcc2c |
1
.gitignore
vendored
1
.gitignore
vendored
@ -40,7 +40,6 @@ wrangler.toml
|
||||
|
||||
dist
|
||||
backend/server/projects
|
||||
backend/database/drizzle
|
||||
|
||||
app.yaml
|
||||
ingressController.yaml
|
28
README.md
28
README.md
@ -2,7 +2,7 @@
|
||||
|
||||
![2024-10-2307 17 42-ezgif com-resize](https://github.com/user-attachments/assets/a4057129-81a7-4a31-a093-c8bc8189ae72)
|
||||
|
||||
Sandbox is an open-source cloud-based code editing environment with custom AI code generation, live preview, real-time collaboration and AI chat.
|
||||
Sandbox is an open-source cloud-based code editing environment with custom AI code generation, live preview, real-time collaboration, and AI chat.
|
||||
|
||||
For the latest updates, join our Discord server: [discord.gitwit.dev](https://discord.gitwit.dev/).
|
||||
|
||||
@ -10,11 +10,11 @@ For the latest updates, join our Discord server: [discord.gitwit.dev](https://di
|
||||
|
||||
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.
|
||||
|
||||
### 0. Requirements
|
||||
|
||||
The application uses NodeJS for the backend, NextJS for the frontend and Cloudflare workers for additional backend tasks.
|
||||
The application uses NodeJS for the backend, NextJS for the frontend, and Cloudflare workers for additional backend tasks.
|
||||
|
||||
Needed accounts to set up:
|
||||
|
||||
@ -22,6 +22,7 @@ Needed accounts to set up:
|
||||
- [Liveblocks](https://liveblocks.io/): Used for collaborative editing.
|
||||
- [E2B](https://e2b.dev/): Used for the terminals and live preview.
|
||||
- [Cloudflare](https://www.cloudflare.com/): Used for relational data storage (D2) and file storage (R2).
|
||||
- [Anthropic](https://anthropic.com/) and [OpenAI](https://openai.com/): API keys for code generation.
|
||||
|
||||
A quick overview of the tech before we start: The deployment uses a **NextJS** app for the frontend and an **ExpressJS** server on the backend. Presumably that's because NextJS integrates well with Clerk middleware but not with Socket.io.
|
||||
|
||||
@ -41,7 +42,6 @@ Run `npm install` in:
|
||||
/backend/database
|
||||
/backend/storage
|
||||
/backend/server
|
||||
/backend/ai
|
||||
```
|
||||
|
||||
### 2. Adding Clerk
|
||||
@ -78,6 +78,8 @@ npx wrangler deploy
|
||||
|
||||
### 4. Deploying the database
|
||||
|
||||
Follow this [guide](https://docs.google.com/document/d/1w5dA5daic_sIYB5Seni1KvnFx51pPV2so6lLdN2xa7Q/edit?usp=sharing) for more info.
|
||||
|
||||
Create a database:
|
||||
|
||||
```
|
||||
@ -152,6 +154,7 @@ NEXT_PUBLIC_DATABASE_WORKER_URL='https://database.🍎.workers.dev'
|
||||
NEXT_PUBLIC_STORAGE_WORKER_URL='https://storage.🍎.workers.dev'
|
||||
NEXT_PUBLIC_WORKERS_KEY='SUPERDUPERSECRET'
|
||||
ANTHROPIC_API_KEY='🔑'
|
||||
OPENAI_API_KEY='🔑'
|
||||
```
|
||||
|
||||
### 10. Running the IDE
|
||||
@ -173,6 +176,15 @@ Setting up deployments first requires a separate domain (such as gitwit.app, whi
|
||||
|
||||
We then deploy Dokku on a separate server, according to this guide: https://dev.to/jamesmurdza/host-your-own-paas-platform-as-a-service-on-amazon-web-services-3f0d
|
||||
|
||||
And we install [dokku-daemon](https://github.com/dokku/dokku-daemon) with the following commands:
|
||||
|
||||
```
|
||||
git clone https://github.com/dokku/dokku-daemon
|
||||
cd dokku-daemon
|
||||
sudo make install
|
||||
systemctl start dokku-daemon
|
||||
```
|
||||
|
||||
The Sandbox platform connects to the Dokku server via SSH, using SSH keys specifically generated for this connection. The SSH key is stored on the Sandbox server, and the following environment variables are set in /backend/server/.env:
|
||||
|
||||
```bash
|
||||
@ -181,6 +193,10 @@ DOKKU_USERNAME=
|
||||
DOKKU_KEY=
|
||||
```
|
||||
|
||||
## Deploying to AWS
|
||||
|
||||
The backend server and deployments server can be deployed using AWS's EC2 service. See [our video guide](https://www.youtube.com/watch?v=WN8HQnimjmk) on how to do this.
|
||||
|
||||
## 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.
|
||||
@ -246,8 +262,7 @@ backend/
|
||||
├── database/
|
||||
│ ├── src
|
||||
│ └── drizzle
|
||||
├── storage
|
||||
└── ai
|
||||
└── storage
|
||||
```
|
||||
|
||||
| Path | Description |
|
||||
@ -256,7 +271,6 @@ backend/
|
||||
| `backend/server` | The Express websocket server. |
|
||||
| `backend/database` | API for interfacing with the D1 database (SQLite). |
|
||||
| `backend/storage` | API for interfacing with R2 storage. Service-bound to `/backend/database`. |
|
||||
| `backend/ai` | API for making requests to Workers AI . |
|
||||
|
||||
### Development
|
||||
|
||||
|
46
backend/database/drizzle/0000_sudden_wallop.sql
Normal file
46
backend/database/drizzle/0000_sudden_wallop.sql
Normal file
@ -0,0 +1,46 @@
|
||||
CREATE TABLE `sandbox` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`type` text NOT NULL,
|
||||
`visibility` text,
|
||||
`createdAt` integer DEFAULT CURRENT_TIMESTAMP,
|
||||
`user_id` text NOT NULL,
|
||||
`likeCount` integer DEFAULT 0,
|
||||
`viewCount` integer DEFAULT 0,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `sandbox_likes` (
|
||||
`user_id` text NOT NULL,
|
||||
`sandbox_id` text NOT NULL,
|
||||
`createdAt` integer DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY(`sandbox_id`, `user_id`),
|
||||
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action,
|
||||
FOREIGN KEY (`sandbox_id`) REFERENCES `sandbox`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `user` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`email` text NOT NULL,
|
||||
`username` text NOT NULL,
|
||||
`avatarUrl` text,
|
||||
`githubToken` text,
|
||||
`createdAt` integer DEFAULT CURRENT_TIMESTAMP,
|
||||
`generations` integer DEFAULT 0,
|
||||
`tier` text DEFAULT 'FREE',
|
||||
`tierExpiresAt` integer,
|
||||
`lastResetDate` integer
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `users_to_sandboxes` (
|
||||
`userId` text NOT NULL,
|
||||
`sandboxId` text NOT NULL,
|
||||
`sharedOn` integer,
|
||||
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action,
|
||||
FOREIGN KEY (`sandboxId`) REFERENCES `sandbox`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `sandbox_id_unique` ON `sandbox` (`id`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `user_id_unique` ON `user` (`id`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `user_username_unique` ON `user` (`username`);
|
3
backend/database/drizzle/0001_dusty_komodo.sql
Normal file
3
backend/database/drizzle/0001_dusty_komodo.sql
Normal file
@ -0,0 +1,3 @@
|
||||
ALTER TABLE user ADD `bio` text;--> statement-breakpoint
|
||||
ALTER TABLE user ADD `personalWebsite` text;--> statement-breakpoint
|
||||
ALTER TABLE user ADD `links` text DEFAULT '[]';
|
@ -0,0 +1 @@
|
||||
ALTER TABLE sandbox ADD `containerId` text;
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": "5",
|
||||
"dialect": "sqlite",
|
||||
"id": "1288b006-6410-4b1c-8c96-d9797878a116",
|
||||
"id": "4ada398d-7e4e-448f-8cea-a10b4d844600",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"sandbox": {
|
||||
@ -94,6 +94,72 @@
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"sandbox_likes": {
|
||||
"name": "sandbox_likes",
|
||||
"columns": {
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"sandbox_id": {
|
||||
"name": "sandbox_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"sandbox_likes_user_id_user_id_fk": {
|
||||
"name": "sandbox_likes_user_id_user_id_fk",
|
||||
"tableFrom": "sandbox_likes",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"sandbox_likes_sandbox_id_sandbox_id_fk": {
|
||||
"name": "sandbox_likes_sandbox_id_sandbox_id_fk",
|
||||
"tableFrom": "sandbox_likes",
|
||||
"tableTo": "sandbox",
|
||||
"columnsFrom": [
|
||||
"sandbox_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"sandbox_likes_sandbox_id_user_id_pk": {
|
||||
"columns": [
|
||||
"sandbox_id",
|
||||
"user_id"
|
||||
],
|
||||
"name": "sandbox_likes_sandbox_id_user_id_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"user": {
|
||||
"name": "user",
|
||||
"columns": {
|
||||
@ -132,6 +198,13 @@
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"githubToken": {
|
||||
"name": "githubToken",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "integer",
|
||||
|
353
backend/database/drizzle/meta/0001_snapshot.json
Normal file
353
backend/database/drizzle/meta/0001_snapshot.json
Normal file
@ -0,0 +1,353 @@
|
||||
{
|
||||
"version": "5",
|
||||
"dialect": "sqlite",
|
||||
"id": "80c0b0b2-bb0e-449a-b447-c21863686f58",
|
||||
"prevId": "4ada398d-7e4e-448f-8cea-a10b4d844600",
|
||||
"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
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP"
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"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": {
|
||||
"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": {}
|
||||
},
|
||||
"sandbox_likes": {
|
||||
"name": "sandbox_likes",
|
||||
"columns": {
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"sandbox_id": {
|
||||
"name": "sandbox_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"sandbox_likes_user_id_user_id_fk": {
|
||||
"name": "sandbox_likes_user_id_user_id_fk",
|
||||
"tableFrom": "sandbox_likes",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"sandbox_likes_sandbox_id_sandbox_id_fk": {
|
||||
"name": "sandbox_likes_sandbox_id_sandbox_id_fk",
|
||||
"tableFrom": "sandbox_likes",
|
||||
"tableTo": "sandbox",
|
||||
"columnsFrom": [
|
||||
"sandbox_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"sandbox_likes_sandbox_id_user_id_pk": {
|
||||
"columns": [
|
||||
"sandbox_id",
|
||||
"user_id"
|
||||
],
|
||||
"name": "sandbox_likes_sandbox_id_user_id_pk"
|
||||
}
|
||||
},
|
||||
"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
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"avatarUrl": {
|
||||
"name": "avatarUrl",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"githubToken": {
|
||||
"name": "githubToken",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": 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
|
||||
},
|
||||
"bio": {
|
||||
"name": "bio",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"personalWebsite": {
|
||||
"name": "personalWebsite",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"links": {
|
||||
"name": "links",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
},
|
||||
"tier": {
|
||||
"name": "tier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'FREE'"
|
||||
},
|
||||
"tierExpiresAt": {
|
||||
"name": "tierExpiresAt",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"lastResetDate": {
|
||||
"name": "lastResetDate",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_id_unique": {
|
||||
"name": "user_id_unique",
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"user_username_unique": {
|
||||
"name": "user_username_unique",
|
||||
"columns": [
|
||||
"username"
|
||||
],
|
||||
"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
|
||||
},
|
||||
"sharedOn": {
|
||||
"name": "sharedOn",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"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": {}
|
||||
}
|
||||
}
|
360
backend/database/drizzle/meta/0002_snapshot.json
Normal file
360
backend/database/drizzle/meta/0002_snapshot.json
Normal file
@ -0,0 +1,360 @@
|
||||
{
|
||||
"version": "5",
|
||||
"dialect": "sqlite",
|
||||
"id": "51abcf01-2921-4885-8058-d1ccd576f3e1",
|
||||
"prevId": "80c0b0b2-bb0e-449a-b447-c21863686f58",
|
||||
"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
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP"
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"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
|
||||
},
|
||||
"containerId": {
|
||||
"name": "containerId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"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": {}
|
||||
},
|
||||
"sandbox_likes": {
|
||||
"name": "sandbox_likes",
|
||||
"columns": {
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"sandbox_id": {
|
||||
"name": "sandbox_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"sandbox_likes_user_id_user_id_fk": {
|
||||
"name": "sandbox_likes_user_id_user_id_fk",
|
||||
"tableFrom": "sandbox_likes",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"sandbox_likes_sandbox_id_sandbox_id_fk": {
|
||||
"name": "sandbox_likes_sandbox_id_sandbox_id_fk",
|
||||
"tableFrom": "sandbox_likes",
|
||||
"tableTo": "sandbox",
|
||||
"columnsFrom": [
|
||||
"sandbox_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"sandbox_likes_sandbox_id_user_id_pk": {
|
||||
"columns": [
|
||||
"sandbox_id",
|
||||
"user_id"
|
||||
],
|
||||
"name": "sandbox_likes_sandbox_id_user_id_pk"
|
||||
}
|
||||
},
|
||||
"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
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"avatarUrl": {
|
||||
"name": "avatarUrl",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"githubToken": {
|
||||
"name": "githubToken",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": 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
|
||||
},
|
||||
"bio": {
|
||||
"name": "bio",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"personalWebsite": {
|
||||
"name": "personalWebsite",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"links": {
|
||||
"name": "links",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
},
|
||||
"tier": {
|
||||
"name": "tier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'FREE'"
|
||||
},
|
||||
"tierExpiresAt": {
|
||||
"name": "tierExpiresAt",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"lastResetDate": {
|
||||
"name": "lastResetDate",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_id_unique": {
|
||||
"name": "user_id_unique",
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"user_username_unique": {
|
||||
"name": "user_username_unique",
|
||||
"columns": [
|
||||
"username"
|
||||
],
|
||||
"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
|
||||
},
|
||||
"sharedOn": {
|
||||
"name": "sharedOn",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"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": {}
|
||||
}
|
||||
}
|
@ -5,8 +5,22 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "5",
|
||||
"when": 1732568535771,
|
||||
"tag": "0000_rapid_korath",
|
||||
"when": 1736155854410,
|
||||
"tag": "0000_sudden_wallop",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "5",
|
||||
"when": 1736169498666,
|
||||
"tag": "0001_dusty_komodo",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "5",
|
||||
"when": 1736768910615,
|
||||
"tag": "0002_chemical_brother_voodoo",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
1960
backend/database/package-lock.json
generated
1960
backend/database/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -18,7 +18,7 @@
|
||||
"drizzle-kit": "^0.20.17",
|
||||
"typescript": "^5.0.4",
|
||||
"vitest": "1.3.0",
|
||||
"wrangler": "^3.86.0"
|
||||
"wrangler": "^3.101.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -2,114 +2,139 @@ import { createId } from "@paralleldrive/cuid2"
|
||||
import { relations, sql } from "drizzle-orm"
|
||||
import { integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core"
|
||||
|
||||
export const KNOWN_PLATFORMS = [
|
||||
"github",
|
||||
"twitter",
|
||||
"instagram",
|
||||
"bluesky",
|
||||
"linkedin",
|
||||
"youtube",
|
||||
"twitch",
|
||||
"discord",
|
||||
"mastodon",
|
||||
"threads",
|
||||
"gitlab",
|
||||
"generic",
|
||||
] as const
|
||||
|
||||
export type KnownPlatform = (typeof KNOWN_PLATFORMS)[number]
|
||||
export type UserLink = {
|
||||
url: string
|
||||
platform: KnownPlatform
|
||||
}
|
||||
// #region Tables
|
||||
export const user = sqliteTable("user", {
|
||||
id: text("id")
|
||||
.$defaultFn(() => createId())
|
||||
.primaryKey()
|
||||
.unique(),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull(),
|
||||
username: text("username").notNull().unique(),
|
||||
avatarUrl: text("avatarUrl"),
|
||||
createdAt: integer("createdAt", { mode: "timestamp_ms" }).default(
|
||||
sql`CURRENT_TIMESTAMP`
|
||||
),
|
||||
generations: integer("generations").default(0),
|
||||
tier: text("tier", { enum: ["FREE", "PRO", "ENTERPRISE"] }).default("FREE"),
|
||||
tierExpiresAt: integer("tierExpiresAt"),
|
||||
lastResetDate: integer("lastResetDate"),
|
||||
id: text("id")
|
||||
.$defaultFn(() => createId())
|
||||
.primaryKey()
|
||||
.unique(),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull(),
|
||||
username: text("username").notNull().unique(),
|
||||
avatarUrl: text("avatarUrl"),
|
||||
githubToken: text("githubToken"),
|
||||
createdAt: integer("createdAt", { mode: "timestamp_ms" }).default(
|
||||
sql`CURRENT_TIMESTAMP`
|
||||
),
|
||||
generations: integer("generations").default(0),
|
||||
bio: text("bio"),
|
||||
personalWebsite: text("personalWebsite"),
|
||||
links: text("links", { mode: "json" }).default("[]").$type<UserLink[]>(),
|
||||
tier: text("tier", { enum: ["FREE", "PRO", "ENTERPRISE"] }).default("FREE"),
|
||||
tierExpiresAt: integer("tierExpiresAt"),
|
||||
lastResetDate: integer("lastResetDate"),
|
||||
})
|
||||
|
||||
export type User = typeof user.$inferSelect
|
||||
|
||||
export const sandbox = sqliteTable("sandbox", {
|
||||
id: text("id")
|
||||
.$defaultFn(() => createId())
|
||||
.primaryKey()
|
||||
.unique(),
|
||||
name: text("name").notNull(),
|
||||
type: text("type").notNull(),
|
||||
visibility: text("visibility", { enum: ["public", "private"] }),
|
||||
createdAt: integer("createdAt", { mode: "timestamp_ms" }).default(
|
||||
sql`CURRENT_TIMESTAMP`
|
||||
),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
likeCount: integer("likeCount").default(0),
|
||||
viewCount: integer("viewCount").default(0),
|
||||
id: text("id")
|
||||
.$defaultFn(() => createId())
|
||||
.primaryKey()
|
||||
.unique(),
|
||||
name: text("name").notNull(),
|
||||
type: text("type").notNull(),
|
||||
visibility: text("visibility", { enum: ["public", "private"] }),
|
||||
createdAt: integer("createdAt", { mode: "timestamp_ms" }).default(
|
||||
sql`CURRENT_TIMESTAMP`
|
||||
),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
likeCount: integer("likeCount").default(0),
|
||||
viewCount: integer("viewCount").default(0),
|
||||
containerId: text("containerId"),
|
||||
})
|
||||
|
||||
export type Sandbox = typeof sandbox.$inferSelect
|
||||
|
||||
export const sandboxLikes = sqliteTable(
|
||||
"sandbox_likes",
|
||||
{
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
sandboxId: text("sandbox_id")
|
||||
.notNull()
|
||||
.references(() => sandbox.id),
|
||||
createdAt: integer("createdAt", { mode: "timestamp_ms" }).default(
|
||||
sql`CURRENT_TIMESTAMP`
|
||||
),
|
||||
},
|
||||
(table) => ({
|
||||
pk: primaryKey({ columns: [table.sandboxId, table.userId] }),
|
||||
})
|
||||
"sandbox_likes",
|
||||
{
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
sandboxId: text("sandbox_id")
|
||||
.notNull()
|
||||
.references(() => sandbox.id),
|
||||
createdAt: integer("createdAt", { mode: "timestamp_ms" }).default(
|
||||
sql`CURRENT_TIMESTAMP`
|
||||
),
|
||||
},
|
||||
(table) => ({
|
||||
pk: primaryKey({ columns: [table.sandboxId, table.userId] }),
|
||||
})
|
||||
)
|
||||
|
||||
export const usersToSandboxes = sqliteTable("users_to_sandboxes", {
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
sandboxId: text("sandboxId")
|
||||
.notNull()
|
||||
.references(() => sandbox.id),
|
||||
sharedOn: integer("sharedOn", { mode: "timestamp_ms" }),
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
sandboxId: text("sandboxId")
|
||||
.notNull()
|
||||
.references(() => sandbox.id),
|
||||
sharedOn: integer("sharedOn", { mode: "timestamp_ms" }),
|
||||
})
|
||||
|
||||
// #region Relations
|
||||
export const userRelations = relations(user, ({ many }) => ({
|
||||
sandbox: many(sandbox),
|
||||
usersToSandboxes: many(usersToSandboxes),
|
||||
likes: many(sandboxLikes),
|
||||
sandbox: many(sandbox),
|
||||
usersToSandboxes: many(usersToSandboxes),
|
||||
likes: many(sandboxLikes),
|
||||
}))
|
||||
|
||||
export const sandboxRelations = relations(sandbox, ({ one, many }) => ({
|
||||
author: one(user, {
|
||||
fields: [sandbox.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
usersToSandboxes: many(usersToSandboxes),
|
||||
likes: many(sandboxLikes),
|
||||
author: one(user, {
|
||||
fields: [sandbox.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
usersToSandboxes: many(usersToSandboxes),
|
||||
likes: many(sandboxLikes),
|
||||
}))
|
||||
|
||||
export const sandboxLikesRelations = relations(sandboxLikes, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [sandboxLikes.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
sandbox: one(sandbox, {
|
||||
fields: [sandboxLikes.sandboxId],
|
||||
references: [sandbox.id],
|
||||
}),
|
||||
user: one(user, {
|
||||
fields: [sandboxLikes.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
sandbox: one(sandbox, {
|
||||
fields: [sandboxLikes.sandboxId],
|
||||
references: [sandbox.id],
|
||||
}),
|
||||
}))
|
||||
|
||||
export const usersToSandboxesRelations = relations(
|
||||
usersToSandboxes,
|
||||
({ one }) => ({
|
||||
group: one(sandbox, {
|
||||
fields: [usersToSandboxes.sandboxId],
|
||||
references: [sandbox.id],
|
||||
}),
|
||||
user: one(user, {
|
||||
fields: [usersToSandboxes.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
})
|
||||
usersToSandboxes,
|
||||
({ one }) => ({
|
||||
group: one(sandbox, {
|
||||
fields: [usersToSandboxes.sandboxId],
|
||||
references: [sandbox.id],
|
||||
}),
|
||||
user: one(user, {
|
||||
fields: [usersToSandboxes.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
})
|
||||
)
|
||||
|
||||
// #endregion
|
||||
|
@ -7,7 +7,6 @@ PORT=4000
|
||||
WORKERS_KEY=
|
||||
DATABASE_WORKER_URL=
|
||||
STORAGE_WORKER_URL=
|
||||
AI_WORKER_URL=
|
||||
E2B_API_KEY=
|
||||
DOKKU_HOST=
|
||||
DOKKU_USERNAME=
|
||||
|
@ -31,9 +31,30 @@ export class DokkuClient extends SSHSocketClient {
|
||||
|
||||
// List all deployed Dokku apps
|
||||
async listApps(): Promise<string[]> {
|
||||
const response = await this.sendCommand("apps:list")
|
||||
// Split the output by newline and remove the header
|
||||
return response.output.split("\n").slice(1)
|
||||
const response = await this.sendCommand("--quiet apps:list")
|
||||
return response.output.split("\n")
|
||||
}
|
||||
|
||||
// Get the creation timestamp of an app
|
||||
async getAppCreatedAt(appName: string): Promise<number> {
|
||||
const response = await this.sendCommand(
|
||||
`apps:report --app-created-at ${appName}`
|
||||
)
|
||||
const createdAt = parseInt(response.output.trim(), 10)
|
||||
|
||||
if (isNaN(createdAt)) {
|
||||
throw new Error(
|
||||
`Failed to retrieve creation timestamp for app ${appName}`
|
||||
)
|
||||
}
|
||||
|
||||
return createdAt
|
||||
}
|
||||
|
||||
// Check if an app exists
|
||||
async appExists(appName: string): Promise<boolean> {
|
||||
const response = await this.sendCommand(`apps:exists ${appName}`)
|
||||
return response.output.includes("App") === false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,7 +92,11 @@ export class FileManager {
|
||||
|
||||
// Convert local file path to remote path
|
||||
private getRemoteFileId(localId: string): string {
|
||||
return `projects/${this.sandboxId}${localId}`
|
||||
return [
|
||||
"projects",
|
||||
this.sandboxId,
|
||||
localId.startsWith("/") ? localId : localId,
|
||||
].join("/")
|
||||
}
|
||||
|
||||
// Convert remote file path to local file path
|
||||
@ -326,13 +330,15 @@ export class FileManager {
|
||||
if (Buffer.byteLength(body, "utf-8") > MAX_BODY_SIZE) {
|
||||
throw new Error("File size too large. Please reduce the file size.")
|
||||
}
|
||||
|
||||
// Save to remote storage
|
||||
await RemoteFileStorage.saveFile(this.getRemoteFileId(fileId), body)
|
||||
|
||||
// Update local file data cache
|
||||
let file = this.fileData.find((f) => f.id === fileId)
|
||||
if (file) {
|
||||
file.data = body
|
||||
} else {
|
||||
// If the file wasn't in our cache, add it
|
||||
file = {
|
||||
id: fileId,
|
||||
data: body,
|
||||
@ -340,7 +346,49 @@ export class FileManager {
|
||||
this.fileData.push(file)
|
||||
}
|
||||
|
||||
await this.sandbox.files.write(path.posix.join(this.dirName, fileId), body)
|
||||
// Save to sandbox filesystem
|
||||
const filePath = path.posix.join(this.dirName, fileId)
|
||||
await this.sandbox.files.write(filePath, body)
|
||||
|
||||
// Instead of updating the entire file structure, just ensure this file exists in it
|
||||
const parts = fileId.split('/').filter(Boolean)
|
||||
let current = this.files
|
||||
let currentPath = ''
|
||||
|
||||
// Navigate/create the path to the file
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
currentPath += '/' + parts[i]
|
||||
let folder = current.find(
|
||||
(f) => f.type === 'folder' && f.name === parts[i]
|
||||
) as TFolder
|
||||
|
||||
if (!folder) {
|
||||
folder = {
|
||||
id: currentPath,
|
||||
type: 'folder',
|
||||
name: parts[i],
|
||||
children: [],
|
||||
}
|
||||
current.push(folder)
|
||||
}
|
||||
current = folder.children
|
||||
}
|
||||
|
||||
// Add/update the file in the structure if it doesn't exist
|
||||
const fileName = parts[parts.length - 1]
|
||||
const existingFile = current.find(
|
||||
(f) => f.type === 'file' && f.name === fileName
|
||||
)
|
||||
|
||||
if (!existingFile) {
|
||||
current.push({
|
||||
id: fileId,
|
||||
type: 'file',
|
||||
name: fileName,
|
||||
})
|
||||
}
|
||||
|
||||
this.refreshFileList?.(this.files)
|
||||
this.fixPermissions()
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,10 @@ export class SSHSocketClient {
|
||||
private conn: Client
|
||||
private config: SSHConfig
|
||||
private socketPath: string
|
||||
private isConnected: boolean = false
|
||||
private _isConnected: boolean = false
|
||||
public get isConnected(): boolean {
|
||||
return this._isConnected
|
||||
}
|
||||
|
||||
// Constructor initializes the SSH client and sets up configuration
|
||||
constructor(config: SSHConfig, socketPath: string) {
|
||||
@ -34,7 +37,7 @@ export class SSHSocketClient {
|
||||
private closeConnection() {
|
||||
console.log("Closing SSH connection...")
|
||||
this.conn.end()
|
||||
this.isConnected = false
|
||||
this._isConnected = false
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
@ -44,17 +47,17 @@ export class SSHSocketClient {
|
||||
this.conn
|
||||
.on("ready", () => {
|
||||
console.log("SSH connection established")
|
||||
this.isConnected = true
|
||||
this._isConnected = true
|
||||
resolve()
|
||||
})
|
||||
.on("error", (err) => {
|
||||
console.error("SSH connection error:", err)
|
||||
this.isConnected = false
|
||||
this._isConnected = false
|
||||
reject(err)
|
||||
})
|
||||
.on("close", () => {
|
||||
console.log("SSH connection closed")
|
||||
this.isConnected = false
|
||||
this._isConnected = false
|
||||
})
|
||||
.connect(this.config)
|
||||
})
|
||||
@ -86,10 +89,13 @@ export class SSHSocketClient {
|
||||
)
|
||||
})
|
||||
.on("data", (data: Buffer) => {
|
||||
// Netcat remains open until it is closed, so we close the connection once we receive data.
|
||||
resolve(data.toString())
|
||||
stream.close()
|
||||
})
|
||||
.stderr.on("data", (data: Buffer) => {
|
||||
reject(new Error(data.toString()))
|
||||
stream.close()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
@ -156,6 +156,40 @@ export class Sandbox {
|
||||
return { success: true, apps: await this.dokkuClient.listApps() }
|
||||
}
|
||||
|
||||
// Handle getting app creation timestamp
|
||||
const handleGetAppCreatedAt: SocketHandler = async ({ appName }) => {
|
||||
if (!this.dokkuClient)
|
||||
throw new Error(
|
||||
"Failed to retrieve app creation timestamp: No Dokku client"
|
||||
)
|
||||
return {
|
||||
success: true,
|
||||
createdAt: await this.dokkuClient.getAppCreatedAt(appName),
|
||||
}
|
||||
}
|
||||
|
||||
// Handle checking if an app exists
|
||||
const handleAppExists: SocketHandler = async ({ appName }) => {
|
||||
if (!this.dokkuClient) {
|
||||
console.log("Failed to check app existence: No Dokku client")
|
||||
return {
|
||||
success: false,
|
||||
}
|
||||
}
|
||||
if (!this.dokkuClient.isConnected) {
|
||||
console.log(
|
||||
"Failed to check app existence: The Dokku client is not connected"
|
||||
)
|
||||
return {
|
||||
success: false,
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
exists: await this.dokkuClient.appExists(appName),
|
||||
}
|
||||
}
|
||||
|
||||
// Handle deploying code
|
||||
const handleDeploy: SocketHandler = async (_: any) => {
|
||||
if (!this.gitClient) throw Error("No git client")
|
||||
@ -253,7 +287,9 @@ export class Sandbox {
|
||||
getFolder: handleGetFolder,
|
||||
saveFile: handleSaveFile,
|
||||
moveFile: handleMoveFile,
|
||||
list: handleListApps,
|
||||
listApps: handleListApps,
|
||||
getAppCreatedAt: handleGetAppCreatedAt,
|
||||
getAppExists: handleAppExists,
|
||||
deploy: handleDeploy,
|
||||
createFile: handleCreateFile,
|
||||
createFolder: handleCreateFolder,
|
||||
|
@ -146,6 +146,8 @@ io.on("connection", async (socket) => {
|
||||
)
|
||||
})
|
||||
|
||||
socket.emit("ready")
|
||||
|
||||
// Handle disconnection event
|
||||
socket.on("disconnect", async () => {
|
||||
try {
|
||||
|
@ -3,18 +3,21 @@ CLERK_SECRET_KEY=
|
||||
NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY=
|
||||
LIVEBLOCKS_SECRET_KEY=
|
||||
|
||||
NEXT_PUBLIC_SERVER_URL=http://localhost:4000
|
||||
NEXT_PUBLIC_SERVER_PORT=4000
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_API_URL=http://localhost:4000
|
||||
NEXT_PUBLIC_SERVER_URL=http://localhost:4000
|
||||
|
||||
# Set WORKER_URLs after deploying the workers.
|
||||
# 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_STORAGE_WORKER_URL=
|
||||
NEXT_PUBLIC_AI_WORKER_URL=
|
||||
NEXT_PUBLIC_WORKERS_KEY=
|
||||
NEXT_PUBLIC_DATABASE_WORKER_URL=https://database.your-worker.workers.dev
|
||||
NEXT_PUBLIC_STORAGE_WORKER_URL=https://storage.your-worker.workers.dev
|
||||
NEXT_PUBLIC_WORKERS_KEY=SUPERDUPERSECRET
|
||||
|
||||
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
|
||||
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
|
||||
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
|
||||
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard
|
||||
|
||||
ANTHROPIC_API_KEY=
|
||||
OPENAI_API_KEY=
|
@ -91,23 +91,17 @@ export default async function CodePage({ params }: { params: { id: string } }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
|
||||
{/* <Room id={sandboxId}> */}
|
||||
<TerminalProvider>
|
||||
<Navbar
|
||||
userData={userData}
|
||||
sandboxData={sandboxData}
|
||||
shared={
|
||||
shared as { id: string; name: string; avatarUrl: string }[]
|
||||
}
|
||||
/>
|
||||
<div className="w-screen flex grow">
|
||||
<CodeEditor userData={userData} sandboxData={sandboxData} />
|
||||
</div>
|
||||
</TerminalProvider>
|
||||
{/* </Room> */}
|
||||
<TerminalProvider>
|
||||
{/* <Room id={sandboxId}> */}
|
||||
<div className="overflow-hidden overscroll-none w-screen h-screen grid [grid-template-rows:3.5rem_auto] bg-background">
|
||||
<Navbar
|
||||
userData={userData}
|
||||
sandboxData={sandboxData}
|
||||
shared={shared as { id: string; name: string; avatarUrl: string }[]}
|
||||
/>
|
||||
<CodeEditor userData={userData} sandboxData={sandboxData} />
|
||||
</div>
|
||||
</>
|
||||
{/* </Room> */}
|
||||
</TerminalProvider>
|
||||
)
|
||||
}
|
||||
|
@ -1,12 +1,59 @@
|
||||
import { currentUser } from "@clerk/nextjs"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { TIERS } from "@/lib/tiers"
|
||||
import {
|
||||
ignoredFiles,
|
||||
ignoredFolders,
|
||||
} from "@/components/editor/AIChat/lib/ignored-paths"
|
||||
import { templateConfigs } from "@/lib/templates"
|
||||
import { TIERS } from "@/lib/tiers"
|
||||
import { TFile, TFolder } from "@/lib/types"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { currentUser } from "@clerk/nextjs"
|
||||
|
||||
const anthropic = new Anthropic({
|
||||
apiKey: process.env.ANTHROPIC_API_KEY!,
|
||||
})
|
||||
|
||||
// Format file structure for context
|
||||
function formatFileStructure(
|
||||
items: (TFile | TFolder)[] | undefined,
|
||||
prefix = ""
|
||||
): string {
|
||||
if (!items || !Array.isArray(items)) {
|
||||
return "No files available"
|
||||
}
|
||||
|
||||
// Sort items to show folders first, then files
|
||||
const sortedItems = [...items].sort((a, b) => {
|
||||
if (a.type === b.type) return a.name.localeCompare(b.name)
|
||||
return a.type === "folder" ? -1 : 1
|
||||
})
|
||||
|
||||
return sortedItems
|
||||
.map((item) => {
|
||||
if (
|
||||
item.type === "file" &&
|
||||
!ignoredFiles.some(
|
||||
(pattern) =>
|
||||
item.name.endsWith(pattern.replace("*", "")) ||
|
||||
item.name === pattern
|
||||
)
|
||||
) {
|
||||
return `${prefix}├── ${item.name}`
|
||||
} else if (
|
||||
item.type === "folder" &&
|
||||
!ignoredFolders.some((folder) => folder === item.name)
|
||||
) {
|
||||
const folderContent = formatFileStructure(
|
||||
item.children,
|
||||
`${prefix}│ `
|
||||
)
|
||||
return `${prefix}├── ${item.name}/\n${folderContent}`
|
||||
}
|
||||
return null
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const user = await currentUser()
|
||||
@ -43,7 +90,8 @@ export async function POST(request: Request) {
|
||||
const userData = await dbUser.json()
|
||||
|
||||
// Get tier settings
|
||||
const tierSettings = TIERS[userData.tier as keyof typeof TIERS] || TIERS.FREE
|
||||
const tierSettings =
|
||||
TIERS[userData.tier as keyof typeof TIERS] || TIERS.FREE
|
||||
if (userData.generations >= tierSettings.generations) {
|
||||
return new Response(
|
||||
`AI generation limit reached for your ${userData.tier || "FREE"} tier`,
|
||||
@ -58,27 +106,32 @@ export async function POST(request: Request) {
|
||||
isEditMode,
|
||||
fileName,
|
||||
line,
|
||||
templateType
|
||||
templateType,
|
||||
files,
|
||||
projectName,
|
||||
} = await request.json()
|
||||
|
||||
// Get template configuration
|
||||
const templateConfig = templateConfigs[templateType]
|
||||
|
||||
// Create template context
|
||||
const templateContext = templateConfig ? `
|
||||
const templateContext = templateConfig
|
||||
? `
|
||||
Project Template: ${templateConfig.name}
|
||||
|
||||
File Structure:
|
||||
${Object.entries(templateConfig.fileStructure)
|
||||
.map(([path, info]) => `${path} - ${info.description}`)
|
||||
.join('\n')}
|
||||
Current File Structure:
|
||||
${files ? formatFileStructure(files) : "No files available"}
|
||||
|
||||
Conventions:
|
||||
${templateConfig.conventions.join('\n')}
|
||||
${templateConfig.conventions.join("\n")}
|
||||
|
||||
Dependencies:
|
||||
${JSON.stringify(templateConfig.dependencies, null, 2)}
|
||||
` : ''
|
||||
|
||||
Scripts:
|
||||
${JSON.stringify(templateConfig.scripts, null, 2)}
|
||||
`
|
||||
: ""
|
||||
|
||||
// Create system message based on mode
|
||||
let systemMessage
|
||||
@ -100,13 +153,23 @@ Instructions: ${messages[0].content}
|
||||
|
||||
Respond only with the modified code that can directly replace the existing code.`
|
||||
} else {
|
||||
systemMessage = `You are an intelligent programming assistant for a ${templateType} project. Please respond to the following request concisely. If your response includes code, please format it using triple backticks (\`\`\`) with the appropriate language identifier. For example:
|
||||
systemMessage = `You are an intelligent programming assistant for a ${templateType} project. Please respond to the following request concisely. When providing code:
|
||||
|
||||
\`\`\`python
|
||||
print("Hello, World!")
|
||||
\`\`\`
|
||||
1. Format it using triple backticks (\`\`\`) with the appropriate language identifier.
|
||||
2. Always specify the complete file path in the format:
|
||||
${projectName}/filepath/to/file.ext
|
||||
|
||||
Provide a clear and concise explanation along with any code snippets. Keep your response brief and to the point.
|
||||
3. If creating a new file, specify the path as:
|
||||
${projectName}/filepath/to/file.ext (new file)
|
||||
|
||||
4. Format your code blocks as:
|
||||
|
||||
${projectName}/filepath/to/file.ext
|
||||
\`\`\`language
|
||||
code here
|
||||
\`\`\`
|
||||
|
||||
If multiple files are involved, repeat the format for each file. Provide a clear and concise explanation along with any code snippets. Keep your response brief and to the point.
|
||||
|
||||
This is the project template:
|
||||
${templateContext}
|
||||
@ -146,7 +209,10 @@ ${activeFileContent ? `Active File Content:\n${activeFileContent}\n` : ""}`
|
||||
new ReadableStream({
|
||||
async start(controller) {
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
|
||||
if (
|
||||
chunk.type === "content_block_delta" &&
|
||||
chunk.delta.type === "text_delta"
|
||||
) {
|
||||
controller.enqueue(encoder.encode(chunk.delta.text))
|
||||
}
|
||||
}
|
||||
|
69
frontend/app/api/merge/route.ts
Normal file
69
frontend/app/api/merge/route.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import OpenAI from "openai"
|
||||
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
})
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { originalCode, newCode, fileName } = await request.json()
|
||||
|
||||
const systemPrompt = `You are a code merging assistant. Your task is to merge the new code snippet with the original file content while:
|
||||
1. Preserving the original file's functionality
|
||||
2. Ensuring proper integration of the new code
|
||||
3. Maintaining consistent style and formatting
|
||||
4. Resolving any potential conflicts
|
||||
5. Output ONLY the raw code without any:
|
||||
- Code fence markers (\`\`\`)
|
||||
- Language identifiers (typescript, javascript, etc.)
|
||||
- Explanations or comments
|
||||
- Markdown formatting
|
||||
|
||||
The output should be the exact code that will replace the existing code, nothing more and nothing less.
|
||||
|
||||
Important: When merging, preserve the original code structure as much as possible. Only make necessary changes to integrate the new code while maintaining the original code's organization and style.`
|
||||
|
||||
const mergedCode = `Original file (${fileName}):\n${originalCode}\n\nNew code to merge:\n${newCode}`
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4o",
|
||||
messages: [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: mergedCode },
|
||||
],
|
||||
prediction: {
|
||||
type: "content",
|
||||
content: mergedCode,
|
||||
},
|
||||
stream: true,
|
||||
})
|
||||
|
||||
// Clean and stream response
|
||||
const encoder = new TextEncoder()
|
||||
return new Response(
|
||||
new ReadableStream({
|
||||
async start(controller) {
|
||||
let buffer = ""
|
||||
for await (const chunk of response) {
|
||||
if (chunk.choices[0]?.delta?.content) {
|
||||
buffer += chunk.choices[0].delta.content
|
||||
// Clean any code fence markers that might appear in the stream
|
||||
const cleanedContent = buffer
|
||||
.replace(/^```[\w-]*\n|```\s*$/gm, "") // Remove code fences
|
||||
.replace(/^(javascript|typescript|python|html|css)\n/gm, "") // Remove language identifiers
|
||||
controller.enqueue(encoder.encode(cleanedContent))
|
||||
buffer = ""
|
||||
}
|
||||
}
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Merge error:", error)
|
||||
return new Response(
|
||||
error instanceof Error ? error.message : "Failed to merge code",
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 25 KiB |
@ -102,26 +102,27 @@
|
||||
.light .gradient-button-bg {
|
||||
background: radial-gradient(
|
||||
circle at top,
|
||||
#262626 0%,
|
||||
#f5f5f5 50%
|
||||
); /* Dark gray -> Light gray */
|
||||
#f5f5f5 0%,
|
||||
/* Very light gray */ #e0e0e0 50% /* Soft gray */
|
||||
);
|
||||
}
|
||||
|
||||
.light .gradient-button {
|
||||
background: radial-gradient(
|
||||
circle at bottom,
|
||||
hsl(0, 10%, 25%) -10%,
|
||||
#9d9d9d 50%
|
||||
); /* Light gray -> Almost white */
|
||||
hsl(0, 0%, 85%) -10%,
|
||||
/* Slightly darker gray */ hsl(0, 0%, 95%) 50% /* Very soft light gray */
|
||||
);
|
||||
}
|
||||
|
||||
.light .gradient-button-bg > div:hover {
|
||||
background: radial-gradient(
|
||||
circle at bottom,
|
||||
hsl(0, 10%, 25%) -10%,
|
||||
#9d9d9d 80%
|
||||
); /* Light gray -> Almost white */
|
||||
hsl(0, 0%, 80%) -10%,
|
||||
/* Slightly darker gray for hover */ hsl(0, 0%, 90%) 80% /* Softer gray */
|
||||
);
|
||||
}
|
||||
|
||||
.inline-decoration::before {
|
||||
content: "Generate";
|
||||
color: #525252;
|
||||
@ -175,3 +176,23 @@
|
||||
.tab-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.added-line-decoration {
|
||||
background-color: rgba(0, 255, 0, 0.1);
|
||||
}
|
||||
|
||||
.removed-line-decoration {
|
||||
background-color: rgba(255, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.added-line-glyph {
|
||||
background-color: #28a745;
|
||||
width: 4px !important;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.removed-line-glyph {
|
||||
background-color: #dc3545;
|
||||
width: 4px !important;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
11
frontend/app/icon.svg
Normal file
11
frontend/app/icon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 17 KiB |
@ -11,7 +11,22 @@ import "./globals.css"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Sandbox",
|
||||
description: "A collaborative, AI-powered cloud code editing environment",
|
||||
description:
|
||||
"an open-source cloud-based code editing environment with custom AI code generation, live preview, real-time collaboration, and AI chat",
|
||||
openGraph: {
|
||||
type: "website",
|
||||
url: "https://sandbox.gitwit.dev",
|
||||
title: "Sandbox",
|
||||
description:
|
||||
"an open-source cloud-based code editing environment with custom AI code generation, live preview, real-time collaboration, and AI chat",
|
||||
},
|
||||
twitter: {
|
||||
site: "https://sandbox.gitwit.dev",
|
||||
title: "Sandbox by Gitwit",
|
||||
description:
|
||||
"an open-source cloud-based code editing environment with custom AI code generation, live preview, real-time collaboration, and AI chat",
|
||||
creator: "@gitwitdev",
|
||||
},
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
|
1
frontend/app/opengraph-image.alt.txt
Normal file
1
frontend/app/opengraph-image.alt.txt
Normal file
@ -0,0 +1 @@
|
||||
About Sandbox by Gitwit
|
BIN
frontend/app/opengraph-image.png
Normal file
BIN
frontend/app/opengraph-image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 465 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 29 KiB |
@ -9,14 +9,19 @@ import DashboardNavbarSearch from "./search"
|
||||
export default function DashboardNavbar({ userData }: { userData: User }) {
|
||||
return (
|
||||
<div className=" py-2 px-4 w-full flex items-center justify-between border-b border-border">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link
|
||||
href="/"
|
||||
className="ring-offset-2 ring-offset-background focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none rounded-sm"
|
||||
>
|
||||
<Image src={Logo} alt="Logo" width={36} height={36} />
|
||||
</Link>
|
||||
<div className="text-sm font-medium flex items-center">Sandbox</div>
|
||||
<h1 className="text-xl">
|
||||
<span className="font-semibold">Sandbox</span>{" "}
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
by gitwit
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<DashboardNavbarSearch />
|
||||
|
@ -240,7 +240,7 @@ function ProjectCardComponent({
|
||||
>
|
||||
<CanvasRevealEffect
|
||||
animationSpeed={3}
|
||||
containerClassName="bg-black"
|
||||
containerClassName="bg-muted"
|
||||
colors={colors[type]}
|
||||
dotSize={2}
|
||||
/>
|
||||
|
77
frontend/components/editor/AIChat/ApplyButton.tsx
Normal file
77
frontend/components/editor/AIChat/ApplyButton.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import { Check, Loader2 } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { Button } from "../../ui/button"
|
||||
|
||||
interface ApplyButtonProps {
|
||||
code: string
|
||||
activeFileName: string
|
||||
activeFileContent: string
|
||||
editorRef: { current: any }
|
||||
onApply: (mergedCode: string, originalCode: string) => void
|
||||
}
|
||||
|
||||
export default function ApplyButton({
|
||||
code,
|
||||
activeFileName,
|
||||
activeFileContent,
|
||||
editorRef,
|
||||
onApply,
|
||||
}: ApplyButtonProps) {
|
||||
const [isApplying, setIsApplying] = useState(false)
|
||||
|
||||
const handleApply = async () => {
|
||||
setIsApplying(true)
|
||||
try {
|
||||
const response = await fetch("/api/merge", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
originalCode: activeFileContent,
|
||||
newCode: String(code),
|
||||
fileName: activeFileName,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text())
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let mergedCode = ""
|
||||
|
||||
if (reader) {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
mergedCode += decoder.decode(value, { stream: true })
|
||||
}
|
||||
}
|
||||
onApply(mergedCode.trim(), activeFileContent)
|
||||
} catch (error) {
|
||||
console.error("Error applying code:", error)
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Failed to apply code changes"
|
||||
)
|
||||
} finally {
|
||||
setIsApplying(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={handleApply}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="p-1 h-6"
|
||||
disabled={isApplying}
|
||||
>
|
||||
{isApplying ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Check className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
@ -13,6 +13,13 @@ export default function ChatMessage({
|
||||
setContext,
|
||||
setIsContextExpanded,
|
||||
socket,
|
||||
handleApplyCode,
|
||||
activeFileName,
|
||||
activeFileContent,
|
||||
editorRef,
|
||||
mergeDecorationsCollection,
|
||||
setMergeDecorationsCollection,
|
||||
selectFile,
|
||||
}: MessageProps) {
|
||||
// State for expanded message index
|
||||
const [expandedMessageIndex, setExpandedMessageIndex] = useState<
|
||||
@ -104,7 +111,14 @@ export default function ChatMessage({
|
||||
const components = createMarkdownComponents(
|
||||
renderCopyButton,
|
||||
renderMarkdownElement,
|
||||
askAboutCode
|
||||
askAboutCode,
|
||||
activeFileName,
|
||||
activeFileContent,
|
||||
editorRef,
|
||||
handleApplyCode,
|
||||
selectFile,
|
||||
mergeDecorationsCollection,
|
||||
setMergeDecorationsCollection,
|
||||
)
|
||||
|
||||
return (
|
||||
@ -112,8 +126,8 @@ export default function ChatMessage({
|
||||
<div
|
||||
className={`relative p-2 rounded-lg ${
|
||||
message.role === "user"
|
||||
? "bg-[#262626] text-white"
|
||||
: "bg-transparent text-white"
|
||||
? "bg-foreground text-background"
|
||||
: "bg-background text-foreground"
|
||||
} max-w-full`}
|
||||
>
|
||||
{/* Render context tabs */}
|
||||
@ -157,7 +171,7 @@ export default function ChatMessage({
|
||||
end: e.target.value.split("\n").length,
|
||||
})
|
||||
}}
|
||||
className="w-full p-2 bg-[#1e1e1e] text-white font-mono text-sm rounded"
|
||||
className="w-full p-2 bg-[#1e1e1e] text-foreground font-mono text-sm rounded"
|
||||
rows={code.split("\n").length}
|
||||
style={{
|
||||
resize: "vertical",
|
||||
@ -201,7 +215,8 @@ export default function ChatMessage({
|
||||
|
||||
// Parse context to tabs for context tabs component
|
||||
function parseContextToTabs(context: string) {
|
||||
const sections = context.split(/(?=File |Code from )/)
|
||||
// Use specific regex patterns to avoid matching import statements
|
||||
const sections = context.split(/(?=File |Code from |Image \d{1,2}:)/)
|
||||
return sections
|
||||
.map((section, index) => {
|
||||
const lines = section.trim().split("\n")
|
||||
@ -211,16 +226,29 @@ function parseContextToTabs(context: string) {
|
||||
// Remove code block markers for display
|
||||
content = content.replace(/^```[\w-]*\n/, "").replace(/\n```$/, "")
|
||||
|
||||
// Determine if the context is a file or code
|
||||
// Determine the type of context
|
||||
const isFile = titleLine.startsWith("File ")
|
||||
const name = titleLine.replace(/^(File |Code from )/, "").replace(":", "")
|
||||
const isImage = titleLine.startsWith("Image ")
|
||||
const type = isFile ? "file" : isImage ? "image" : "code"
|
||||
const name = titleLine
|
||||
.replace(/^(File |Code from |Image )/, "")
|
||||
.replace(":", "")
|
||||
.trim()
|
||||
|
||||
// Skip if the content is empty or if it's just an import statement
|
||||
if (!content || content.trim().startsWith('from "')) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id: `context-${index}`,
|
||||
type: isFile ? ("file" as const) : ("code" as const),
|
||||
type: type as "file" | "code" | "image",
|
||||
name: name,
|
||||
content: content,
|
||||
}
|
||||
})
|
||||
.filter((tab) => tab.content.length > 0)
|
||||
.filter(
|
||||
(tab): tab is NonNullable<typeof tab> =>
|
||||
tab !== null && tab.content.length > 0
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { useSocket } from "@/context/SocketContext"
|
||||
import { TFile } from "@/lib/types"
|
||||
import { X, ChevronDown } from "lucide-react"
|
||||
import { ChevronDown, X } from "lucide-react"
|
||||
import { nanoid } from "nanoid"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import LoadingDots from "../../ui/LoadingDots"
|
||||
@ -18,6 +19,11 @@ export default function AIChat({
|
||||
lastCopiedRangeRef,
|
||||
files,
|
||||
templateType,
|
||||
handleApplyCode,
|
||||
selectFile,
|
||||
mergeDecorationsCollection,
|
||||
setMergeDecorationsCollection,
|
||||
projectName,
|
||||
}: AIChatProps) {
|
||||
// Initialize socket and messages
|
||||
const { socket } = useSocket()
|
||||
@ -75,8 +81,8 @@ export default function AIChat({
|
||||
useEffect(() => {
|
||||
const container = chatContainerRef.current
|
||||
if (container) {
|
||||
container.addEventListener('scroll', handleScroll)
|
||||
return () => container.removeEventListener('scroll', handleScroll)
|
||||
container.addEventListener("scroll", handleScroll)
|
||||
return () => container.removeEventListener("scroll", handleScroll)
|
||||
}
|
||||
}, [])
|
||||
|
||||
@ -125,6 +131,8 @@ export default function AIChat({
|
||||
} else if (tab.type === "code") {
|
||||
const cleanContent = formatCodeContent(tab.content)
|
||||
return `Code from ${tab.name}:\n\`\`\`typescript\n${cleanContent}\n\`\`\``
|
||||
} else if (tab.type === "image") {
|
||||
return `Image ${tab.name}:\n${tab.content}`
|
||||
}
|
||||
return `${tab.name}:\n${tab.content}`
|
||||
})
|
||||
@ -146,10 +154,10 @@ export default function AIChat({
|
||||
abortControllerRef,
|
||||
activeFileContent,
|
||||
false,
|
||||
templateType
|
||||
templateType,
|
||||
files,
|
||||
projectName
|
||||
)
|
||||
// Clear context tabs after sending
|
||||
setContextTabs([])
|
||||
}
|
||||
|
||||
// Set context for the chat
|
||||
@ -167,6 +175,22 @@ export default function AIChat({
|
||||
addContextTab("code", name, context, range)
|
||||
}
|
||||
|
||||
// update context tabs when file contents change
|
||||
useEffect(() => {
|
||||
setContextTabs((prevTabs) =>
|
||||
prevTabs.map((tab) => {
|
||||
if (tab.type === "file" && tab.name === activeFileName) {
|
||||
const fileExt = tab.name.split(".").pop() || "txt"
|
||||
return {
|
||||
...tab,
|
||||
content: `\`\`\`${fileExt}\n${activeFileContent}\n\`\`\``,
|
||||
}
|
||||
}
|
||||
return tab
|
||||
})
|
||||
)
|
||||
}, [activeFileContent, activeFileName])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen w-full">
|
||||
<div className="flex justify-between items-center p-2 border-b">
|
||||
@ -185,9 +209,9 @@ export default function AIChat({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
<ScrollArea
|
||||
ref={chatContainerRef}
|
||||
className="flex-grow overflow-y-auto p-4 space-y-4 relative"
|
||||
className="flex-grow p-4 space-y-4 relative"
|
||||
>
|
||||
{messages.map((message, messageIndex) => (
|
||||
// Render chat message component for each message
|
||||
@ -197,6 +221,13 @@ export default function AIChat({
|
||||
setContext={setContext}
|
||||
setIsContextExpanded={setIsContextExpanded}
|
||||
socket={socket}
|
||||
handleApplyCode={handleApplyCode}
|
||||
activeFileName={activeFileName}
|
||||
activeFileContent={activeFileContent}
|
||||
editorRef={editorRef}
|
||||
mergeDecorationsCollection={mergeDecorationsCollection}
|
||||
setMergeDecorationsCollection={setMergeDecorationsCollection}
|
||||
selectFile={selectFile}
|
||||
/>
|
||||
))}
|
||||
{isLoading && <LoadingDots />}
|
||||
@ -211,7 +242,7 @@ export default function AIChat({
|
||||
<ChevronDown className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<div className="p-4 border-t mb-14">
|
||||
{/* Render context tabs component */}
|
||||
<ContextTabs
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { TFile, TFolder } from "@/lib/types"
|
||||
import React from "react"
|
||||
|
||||
// Stringify content for chat message component
|
||||
@ -91,7 +92,9 @@ export const handleSend = async (
|
||||
abortControllerRef: React.MutableRefObject<AbortController | null>,
|
||||
activeFileContent: string,
|
||||
isEditMode: boolean = false,
|
||||
templateType: string
|
||||
templateType: string,
|
||||
files: (TFile | TFolder)[],
|
||||
projectName: string
|
||||
) => {
|
||||
// Return if input is empty and context is null
|
||||
if (input.trim() === "" && !context) return
|
||||
@ -131,22 +134,22 @@ export const handleSend = async (
|
||||
}))
|
||||
|
||||
// Fetch AI response for chat message component
|
||||
const response = await fetch("/api/ai",
|
||||
{
|
||||
const response = await fetch("/api/ai", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: anthropicMessages,
|
||||
context: context || undefined,
|
||||
activeFileContent: activeFileContent,
|
||||
isEditMode: isEditMode,
|
||||
templateType: templateType,
|
||||
}),
|
||||
signal: abortControllerRef.current.signal,
|
||||
}
|
||||
)
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: anthropicMessages,
|
||||
context: context || undefined,
|
||||
activeFileContent: activeFileContent,
|
||||
isEditMode: isEditMode,
|
||||
templateType: templateType,
|
||||
files: files,
|
||||
projectName: projectName,
|
||||
}),
|
||||
signal: abortControllerRef.current.signal,
|
||||
})
|
||||
|
||||
// Throw error if response is not ok
|
||||
if (!response.ok) {
|
||||
@ -201,7 +204,8 @@ export const handleSend = async (
|
||||
console.error("Error fetching AI response:", error)
|
||||
const errorMessage = {
|
||||
role: "assistant" as const,
|
||||
content: error.message || "Sorry, I encountered an error. Please try again.",
|
||||
content:
|
||||
error.message || "Sorry, I encountered an error. Please try again.",
|
||||
}
|
||||
setMessages((prev) => [...prev, errorMessage])
|
||||
}
|
||||
@ -239,3 +243,11 @@ export const looksLikeCode = (text: string): boolean => {
|
||||
|
||||
return codeIndicators.some((pattern) => pattern.test(text))
|
||||
}
|
||||
|
||||
// Add this new function after looksLikeCode function
|
||||
export const isFilePath = (text: string): boolean => {
|
||||
// Match patterns like next/styles/SignIn.module.css or path/to/file.ext (new file)
|
||||
const pattern =
|
||||
/^(?:[a-zA-Z0-9_.-]+\/)*[a-zA-Z0-9_.-]+\.[a-zA-Z0-9]+(\s+\(new file\))?$/
|
||||
return pattern.test(text)
|
||||
}
|
||||
|
@ -1,15 +1,26 @@
|
||||
import { CornerUpLeft } from "lucide-react"
|
||||
import { useSocket } from "@/context/SocketContext"
|
||||
import { TTab } from "@/lib/types"
|
||||
import { Check, CornerUpLeft, FileText, X } from "lucide-react"
|
||||
import monaco from "monaco-editor"
|
||||
import { Components } from "react-markdown"
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
||||
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"
|
||||
import { Button } from "../../../ui/button"
|
||||
import { stringifyContent } from "./chatUtils"
|
||||
import ApplyButton from "../ApplyButton"
|
||||
import { isFilePath, stringifyContent } from "./chatUtils"
|
||||
|
||||
// Create markdown components for chat message component
|
||||
export const createMarkdownComponents = (
|
||||
renderCopyButton: (text: any) => JSX.Element,
|
||||
renderMarkdownElement: (props: any) => JSX.Element,
|
||||
askAboutCode: (code: any) => void
|
||||
askAboutCode: (code: any) => void,
|
||||
activeFileName: string,
|
||||
activeFileContent: string,
|
||||
editorRef: any,
|
||||
handleApplyCode: (mergedCode: string, originalCode: string) => void,
|
||||
selectFile: (tab: TTab) => void,
|
||||
mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection,
|
||||
setMergeDecorationsCollection?: (collection: undefined) => void
|
||||
): Components => ({
|
||||
code: ({
|
||||
node,
|
||||
@ -25,39 +36,92 @@ export const createMarkdownComponents = (
|
||||
const match = /language-(\w+)/.exec(className || "")
|
||||
|
||||
return match ? (
|
||||
<div className="relative border border-input rounded-md my-4">
|
||||
<div className="absolute top-0 left-0 px-2 py-1 text-xs font-semibold text-gray-200 bg-#1e1e1e rounded-tl">
|
||||
<div className="relative border border-input rounded-md mt-8 my-2 translate-y-[-1rem]">
|
||||
<div className="absolute top-0 left-0 px-2 py-1 text-xs font-semibold text-gray-200 rounded-tl">
|
||||
{match[1]}
|
||||
</div>
|
||||
<div className="absolute top-0 right-0 flex">
|
||||
{renderCopyButton(children)}
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
askAboutCode(children)
|
||||
}}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="p-1 h-6"
|
||||
>
|
||||
<CornerUpLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="pt-6">
|
||||
<SyntaxHighlighter
|
||||
style={vscDarkPlus as any}
|
||||
language={match[1]}
|
||||
PreTag="div"
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: "0.5rem",
|
||||
fontSize: "0.875rem",
|
||||
}}
|
||||
>
|
||||
{stringifyContent(children)}
|
||||
</SyntaxHighlighter>
|
||||
<div className="sticky top-0 right-0 flex justify-end z-10">
|
||||
<div className="flex border border-input shadow-lg bg-background rounded-md">
|
||||
{renderCopyButton(children)}
|
||||
<div className="w-px bg-input"></div>
|
||||
{!mergeDecorationsCollection ? (
|
||||
<ApplyButton
|
||||
code={String(children)}
|
||||
activeFileName={activeFileName}
|
||||
activeFileContent={activeFileContent}
|
||||
editorRef={editorRef}
|
||||
onApply={handleApplyCode}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (
|
||||
setMergeDecorationsCollection &&
|
||||
mergeDecorationsCollection &&
|
||||
editorRef?.current
|
||||
) {
|
||||
mergeDecorationsCollection?.clear()
|
||||
setMergeDecorationsCollection(undefined)
|
||||
}
|
||||
}}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="p-1 h-6"
|
||||
title="Accept Changes"
|
||||
>
|
||||
<Check className="w-4 h-4 text-green-500" />
|
||||
</Button>
|
||||
<div className="w-px bg-input"></div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (editorRef?.current && mergeDecorationsCollection) {
|
||||
const model = editorRef.current.getModel()
|
||||
if (model && (model as any).originalContent) {
|
||||
editorRef.current?.setValue(
|
||||
(model as any).originalContent
|
||||
)
|
||||
mergeDecorationsCollection.clear()
|
||||
setMergeDecorationsCollection?.(undefined)
|
||||
}
|
||||
}
|
||||
}}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="p-1 h-6"
|
||||
title="Discard Changes"
|
||||
>
|
||||
<X className="w-4 h-4 text-red-500" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<div className="w-px bg-input"></div>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
askAboutCode(children)
|
||||
}}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="p-1 h-6"
|
||||
>
|
||||
<CornerUpLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<SyntaxHighlighter
|
||||
style={vscDarkPlus as any}
|
||||
language={match[1]}
|
||||
PreTag="div"
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: "0.5rem",
|
||||
fontSize: "0.875rem",
|
||||
}}
|
||||
>
|
||||
{stringifyContent(children)}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
@ -66,8 +130,62 @@ export const createMarkdownComponents = (
|
||||
)
|
||||
},
|
||||
// Render markdown elements
|
||||
p: ({ node, children, ...props }) =>
|
||||
renderMarkdownElement({ node, children, ...props }),
|
||||
p: ({ node, children, ...props }) => {
|
||||
const content = stringifyContent(children)
|
||||
const { socket } = useSocket()
|
||||
|
||||
if (isFilePath(content)) {
|
||||
const isNewFile = content.endsWith("(new file)")
|
||||
const filePath = (
|
||||
isNewFile ? content.replace(" (new file)", "") : content
|
||||
)
|
||||
.split("/")
|
||||
.filter((part, index) => index !== 0)
|
||||
.join("/")
|
||||
|
||||
const handleFileClick = () => {
|
||||
if (isNewFile) {
|
||||
socket?.emit(
|
||||
"createFile",
|
||||
{
|
||||
name: filePath,
|
||||
},
|
||||
(response: any) => {
|
||||
if (response.success) {
|
||||
const tab: TTab = {
|
||||
id: filePath,
|
||||
name: filePath.split("/").pop() || "",
|
||||
saved: true,
|
||||
type: "file",
|
||||
}
|
||||
selectFile(tab)
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
const tab: TTab = {
|
||||
id: filePath,
|
||||
name: filePath.split("/").pop() || "",
|
||||
saved: true,
|
||||
type: "file",
|
||||
}
|
||||
selectFile(tab)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={handleFileClick}
|
||||
className="group flex items-center gap-2 px-2 py-1 bg-secondary/50 rounded-md my-2 text-xs hover:bg-secondary cursor-pointer w-fit"
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
<span className="font-mono group-hover:underline">{content}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return renderMarkdownElement({ node, children, ...props })
|
||||
},
|
||||
h1: ({ node, children, ...props }) =>
|
||||
renderMarkdownElement({ node, children, ...props }),
|
||||
h2: ({ node, children, ...props }) =>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { TemplateConfig } from "@/lib/templates"
|
||||
import { TFile, TFolder } from "@/lib/types"
|
||||
import { TFile, TFolder, TTab } from "@/lib/types"
|
||||
import * as monaco from "monaco-editor"
|
||||
import { Socket } from "socket.io-client"
|
||||
|
||||
@ -56,6 +56,11 @@ export interface AIChatProps {
|
||||
files: (TFile | TFolder)[]
|
||||
templateType: string
|
||||
templateConfig?: TemplateConfig
|
||||
projectName: string
|
||||
handleApplyCode: (mergedCode: string, originalCode: string) => void
|
||||
mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection
|
||||
setMergeDecorationsCollection?: (collection: undefined) => void
|
||||
selectFile: (tab: TTab) => void
|
||||
}
|
||||
|
||||
// Chat input props interface
|
||||
@ -105,6 +110,13 @@ export interface MessageProps {
|
||||
) => void
|
||||
setIsContextExpanded: (isExpanded: boolean) => void
|
||||
socket: Socket | null
|
||||
handleApplyCode: (mergedCode: string, originalCode: string) => void
|
||||
activeFileName: string
|
||||
activeFileContent: string
|
||||
editorRef: any
|
||||
mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection
|
||||
setMergeDecorationsCollection?: (collection: undefined) => void
|
||||
selectFile: (tab: TTab) => void
|
||||
}
|
||||
|
||||
// Context tabs props interface
|
||||
|
@ -66,15 +66,17 @@ export default function GenerateInput({
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: [{
|
||||
role: "user",
|
||||
content: regenerate ? currentPrompt : input
|
||||
}],
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: regenerate ? currentPrompt : input,
|
||||
},
|
||||
],
|
||||
context: null,
|
||||
activeFileContent: data.code,
|
||||
isEditMode: true,
|
||||
fileName: data.fileName,
|
||||
line: data.line
|
||||
line: data.line,
|
||||
}),
|
||||
})
|
||||
|
||||
|
@ -62,7 +62,7 @@ export default function CodeEditor({
|
||||
//SocketContext functions and effects
|
||||
const { socket, setUserAndSandboxId } = useSocket()
|
||||
// theme
|
||||
const { theme } = useTheme()
|
||||
const { resolvedTheme: theme } = useTheme()
|
||||
useEffect(() => {
|
||||
// Ensure userData.id and sandboxData.id are available before attempting to connect
|
||||
if (userData.id && sandboxData.id) {
|
||||
@ -104,6 +104,13 @@ export default function CodeEditor({
|
||||
// Added this state to track the most recent content for each file
|
||||
const [fileContents, setFileContents] = useState<Record<string, string>>({})
|
||||
|
||||
// Apply Button merger decoration state
|
||||
const [mergeDecorations, setMergeDecorations] = useState<
|
||||
monaco.editor.IModelDeltaDecoration[]
|
||||
>([])
|
||||
const [mergeDecorationsCollection, setMergeDecorationsCollection] =
|
||||
useState<monaco.editor.IEditorDecorationsCollection>()
|
||||
|
||||
// Editor state
|
||||
const [editorLanguage, setEditorLanguage] = useState("plaintext")
|
||||
const [cursorLine, setCursorLine] = useState(0)
|
||||
@ -375,6 +382,57 @@ export default function CodeEditor({
|
||||
})
|
||||
}, [editorRef])
|
||||
|
||||
// handle apply code
|
||||
const handleApplyCode = useCallback(
|
||||
(mergedCode: string, originalCode: string) => {
|
||||
if (!editorRef) return
|
||||
|
||||
const model = editorRef.getModel()
|
||||
if (!model) return // Store original content
|
||||
;(model as any).originalContent = originalCode
|
||||
|
||||
// Calculate the full range of the document
|
||||
const fullRange = model.getFullModelRange()
|
||||
|
||||
// Create decorations before applying the edit
|
||||
const originalLines = originalCode.split("\n")
|
||||
const mergedLines = mergedCode.split("\n")
|
||||
const decorations: monaco.editor.IModelDeltaDecoration[] = []
|
||||
|
||||
for (
|
||||
let i = 0;
|
||||
i < Math.max(originalLines.length, mergedLines.length);
|
||||
i++
|
||||
) {
|
||||
// Only highlight new lines (green highlights)
|
||||
if (i >= originalLines.length || originalLines[i] !== mergedLines[i]) {
|
||||
decorations.push({
|
||||
range: new monaco.Range(i + 1, 1, i + 1, 1),
|
||||
options: {
|
||||
isWholeLine: true,
|
||||
className: "added-line-decoration",
|
||||
glyphMarginClassName: "added-line-glyph",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the edit operation
|
||||
editorRef.executeEdits("apply-code", [
|
||||
{
|
||||
range: fullRange,
|
||||
text: mergedCode,
|
||||
forceMoveMarkers: true,
|
||||
},
|
||||
])
|
||||
|
||||
// Apply decorations after the edit
|
||||
const newDecorations = editorRef.createDecorationsCollection(decorations)
|
||||
setMergeDecorationsCollection(newDecorations)
|
||||
},
|
||||
[editorRef]
|
||||
)
|
||||
|
||||
// Generate widget effect
|
||||
useEffect(() => {
|
||||
if (generate.show) {
|
||||
@ -735,31 +793,32 @@ export default function CodeEditor({
|
||||
setGenerate((prev) => ({ ...prev, show: false }))
|
||||
|
||||
// Check if the tab already exists in the list of open tabs
|
||||
const exists = tabs.find((t) => t.id === tab.id)
|
||||
setTabs((prev) => {
|
||||
if (exists) {
|
||||
// If the tab exists, make it the active tab
|
||||
setActiveFileId(exists.id)
|
||||
return prev
|
||||
}
|
||||
// If the tab doesn't exist, add it to the list of tabs and make it active
|
||||
return [...prev, tab]
|
||||
})
|
||||
const existingTab = tabs.find((t) => t.id === tab.id)
|
||||
|
||||
// If the file's content is already cached, set it as the active content
|
||||
if (fileContents[tab.id]) {
|
||||
setActiveFileContent(fileContents[tab.id])
|
||||
if (existingTab) {
|
||||
// If the tab exists, just make it active
|
||||
setActiveFileId(existingTab.id)
|
||||
if (fileContents[existingTab.id]) {
|
||||
setActiveFileContent(fileContents[existingTab.id])
|
||||
}
|
||||
} else {
|
||||
// Otherwise, fetch the content of the file and cache it
|
||||
debouncedGetFile(tab.id, (response: string) => {
|
||||
setFileContents((prev) => ({ ...prev, [tab.id]: response }))
|
||||
setActiveFileContent(response)
|
||||
})
|
||||
// If the tab doesn't exist, add it to the list and make it active
|
||||
setTabs((prev) => [...prev, tab])
|
||||
|
||||
// Fetch content if not cached
|
||||
if (!fileContents[tab.id]) {
|
||||
debouncedGetFile(tab.id, (response: string) => {
|
||||
setFileContents((prev) => ({ ...prev, [tab.id]: response }))
|
||||
setActiveFileContent(response)
|
||||
})
|
||||
} else {
|
||||
setActiveFileContent(fileContents[tab.id])
|
||||
}
|
||||
}
|
||||
|
||||
// Set the editor language based on the file type
|
||||
setEditorLanguage(processFileType(tab.name))
|
||||
// Set the active file ID to the new tab
|
||||
// Set the active file ID
|
||||
setActiveFileId(tab.id)
|
||||
}
|
||||
|
||||
@ -921,9 +980,9 @@ export default function CodeEditor({
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Copilot DOM elements */}
|
||||
<div className="flex max-h-full overflow-hidden">
|
||||
<PreviewProvider>
|
||||
{/* Copilot DOM elements */}
|
||||
<div ref={generateRef} />
|
||||
<div ref={suggestionRef} className="absolute">
|
||||
<AnimatePresence>
|
||||
@ -1087,62 +1146,62 @@ export default function CodeEditor({
|
||||
</div>
|
||||
</>
|
||||
) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643
|
||||
clerk.loaded ? (
|
||||
<>
|
||||
{/* {provider && userInfo ? (
|
||||
clerk.loaded ? (
|
||||
<>
|
||||
{/* {provider && userInfo ? (
|
||||
<Cursors yProvider={provider} userInfo={userInfo} />
|
||||
) : null} */}
|
||||
<Editor
|
||||
height="100%"
|
||||
language={editorLanguage}
|
||||
beforeMount={handleEditorWillMount}
|
||||
onMount={handleEditorMount}
|
||||
onChange={(value) => {
|
||||
// If the new content is different from the cached content, update it
|
||||
if (value !== fileContents[activeFileId]) {
|
||||
setActiveFileContent(value ?? "") // Update the active file content
|
||||
// Mark the file as unsaved by setting 'saved' to false
|
||||
setTabs((prev) =>
|
||||
prev.map((tab) =>
|
||||
tab.id === activeFileId
|
||||
? { ...tab, saved: false }
|
||||
: tab
|
||||
)
|
||||
<Editor
|
||||
height="100%"
|
||||
language={editorLanguage}
|
||||
beforeMount={handleEditorWillMount}
|
||||
onMount={handleEditorMount}
|
||||
onChange={(value) => {
|
||||
// If the new content is different from the cached content, update it
|
||||
if (value !== fileContents[activeFileId]) {
|
||||
setActiveFileContent(value ?? "") // Update the active file content
|
||||
// Mark the file as unsaved by setting 'saved' to false
|
||||
setTabs((prev) =>
|
||||
prev.map((tab) =>
|
||||
tab.id === activeFileId
|
||||
? { ...tab, saved: false }
|
||||
: tab
|
||||
)
|
||||
} else {
|
||||
// If the content matches the cached content, mark the file as saved
|
||||
setTabs((prev) =>
|
||||
prev.map((tab) =>
|
||||
tab.id === activeFileId
|
||||
? { ...tab, saved: true }
|
||||
: tab
|
||||
)
|
||||
)
|
||||
} else {
|
||||
// If the content matches the cached content, mark the file as saved
|
||||
setTabs((prev) =>
|
||||
prev.map((tab) =>
|
||||
tab.id === activeFileId
|
||||
? { ...tab, saved: true }
|
||||
: tab
|
||||
)
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
tabSize: 2,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
padding: {
|
||||
bottom: 4,
|
||||
top: 4,
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
fixedOverflowWidgets: true,
|
||||
fontFamily: "var(--font-geist-mono)",
|
||||
}}
|
||||
theme={theme === "light" ? "vs" : "vs-dark"}
|
||||
value={activeFileContent}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
|
||||
<Loader2 className="animate-spin w-6 h-6 mr-3" />
|
||||
Waiting for Clerk to load...
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
tabSize: 2,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
padding: {
|
||||
bottom: 4,
|
||||
top: 4,
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
fixedOverflowWidgets: true,
|
||||
fontFamily: "var(--font-geist-mono)",
|
||||
}}
|
||||
theme={theme === "light" ? "vs" : "vs-dark"}
|
||||
value={activeFileContent}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
|
||||
<Loader2 className="animate-spin w-6 h-6 mr-3" />
|
||||
Waiting for Clerk to load...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle />
|
||||
@ -1234,13 +1293,18 @@ export default function CodeEditor({
|
||||
lastCopiedRangeRef={lastCopiedRangeRef}
|
||||
files={files}
|
||||
templateType={sandboxData.type}
|
||||
projectName={sandboxData.name}
|
||||
handleApplyCode={handleApplyCode}
|
||||
mergeDecorationsCollection={mergeDecorationsCollection}
|
||||
setMergeDecorationsCollection={setMergeDecorationsCollection}
|
||||
selectFile={selectFile}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</>
|
||||
)}
|
||||
</ResizablePanelGroup>
|
||||
</PreviewProvider>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
import { useTerminal } from "@/context/TerminalContext"
|
||||
import { Sandbox, User } from "@/lib/types"
|
||||
import { Globe } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
export default function DeployButtonModal({
|
||||
userData,
|
||||
@ -18,8 +18,21 @@ export default function DeployButtonModal({
|
||||
userData: User
|
||||
data: Sandbox
|
||||
}) {
|
||||
const { deploy } = useTerminal()
|
||||
const { deploy, getAppExists } = useTerminal()
|
||||
const [isDeploying, setIsDeploying] = useState(false)
|
||||
const [isDeployed, setIsDeployed] = useState(false)
|
||||
const [deployButtonVisible, setDeployButtonEnabled] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const checkDeployment = async () => {
|
||||
if (getAppExists) {
|
||||
const exists = await getAppExists(data.id)
|
||||
setDeployButtonEnabled(exists.success)
|
||||
setIsDeployed((exists.success && exists.exists) ?? false)
|
||||
}
|
||||
}
|
||||
checkDeployment()
|
||||
}, [data.id, getAppExists])
|
||||
|
||||
const handleDeploy = () => {
|
||||
if (isDeploying) {
|
||||
@ -30,6 +43,7 @@ export default function DeployButtonModal({
|
||||
setIsDeploying(true)
|
||||
deploy(() => {
|
||||
setIsDeploying(false)
|
||||
setIsDeployed(true)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -38,10 +52,12 @@ export default function DeployButtonModal({
|
||||
<>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Globe className="w-4 h-4 mr-2" />
|
||||
Deploy
|
||||
</Button>
|
||||
{deployButtonVisible && (
|
||||
<Button variant="outline">
|
||||
<Globe className="w-4 h-4 mr-2" />
|
||||
Deploy
|
||||
</Button>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="p-4 w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl rounded-lg shadow-lg"
|
||||
@ -52,8 +68,9 @@ export default function DeployButtonModal({
|
||||
<DeploymentOption
|
||||
icon={<Globe className="text-gray-500 w-5 h-5" />}
|
||||
domain={`${data.id}.gitwit.app`}
|
||||
timestamp="Deployed 1h ago"
|
||||
timestamp="Deployed 1m ago"
|
||||
user={userData.name}
|
||||
isDeployed={isDeployed}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
@ -61,7 +78,7 @@ export default function DeployButtonModal({
|
||||
className="mt-4 w-full bg-[#0a0a0a] text-white hover:bg-[#262626]"
|
||||
onClick={handleDeploy}
|
||||
>
|
||||
{isDeploying ? "Deploying..." : "Update"}
|
||||
{isDeploying ? "Deploying..." : isDeployed ? "Update" : "Deploy"}
|
||||
</Button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
@ -74,27 +91,33 @@ function DeploymentOption({
|
||||
domain,
|
||||
timestamp,
|
||||
user,
|
||||
isDeployed,
|
||||
}: {
|
||||
icon: React.ReactNode
|
||||
domain: string
|
||||
timestamp: string
|
||||
user: string
|
||||
isDeployed: boolean
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full text-left p-2 rounded-md border border-gray-700 bg-gray-900">
|
||||
<div className="flex items-start gap-2 relative">
|
||||
<div className="flex-shrink-0">{icon}</div>
|
||||
<a
|
||||
href={`https://${domain}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-semibold text-gray-300 hover:underline"
|
||||
>
|
||||
{domain}
|
||||
</a>
|
||||
{isDeployed ? (
|
||||
<a
|
||||
href={`https://${domain}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-semibold text-gray-300 hover:underline"
|
||||
>
|
||||
{domain}
|
||||
</a>
|
||||
) : (
|
||||
<span className="font-semibold text-gray-300">{domain}</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 mt-0 ml-7">
|
||||
{timestamp} • {user}
|
||||
{isDeployed ? `${timestamp} • ${user}` : "Never deployed"}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
@ -9,6 +9,7 @@ import SidebarFolder from "./folder"
|
||||
import New from "./new"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { cn, sortFileExplorer } from "@/lib/utils"
|
||||
import {
|
||||
@ -104,8 +105,8 @@ export default function Sidebar({
|
||||
|
||||
return (
|
||||
<div className="h-full w-56 select-none flex flex-col text-sm">
|
||||
<div className="flex-grow overflow-auto p-2 pb-[84px]">
|
||||
<div className="flex w-full items-center justify-between h-8 mb-1">
|
||||
<ScrollArea className="flex-grow overflow-auto px-2 pt-0 pb-4 relative">
|
||||
<div className="flex w-full items-center justify-between h-8 pb-1 isolate z-10 sticky pt-2 top-0 bg-background">
|
||||
<div className="text-muted-foreground">Explorer</div>
|
||||
<div className="flex space-x-1">
|
||||
<button
|
||||
@ -179,8 +180,8 @@ export default function Sidebar({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="fixed bottom-0 w-48 flex flex-col p-2 bg-background">
|
||||
</ScrollArea>
|
||||
<div className="flex flex-col p-2 bg-background">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-start text-sm text-muted-foreground font-normal h-8 px-2 mb-2"
|
||||
|
@ -7,7 +7,7 @@ import "./xterm.css"
|
||||
import { debounce } from "@/lib/utils"
|
||||
import { Loader2 } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
import { ElementRef, useEffect, useRef } from "react"
|
||||
import { ElementRef, useEffect, useRef, useCallback } from "react"
|
||||
import { Socket } from "socket.io-client"
|
||||
export default function EditorTerminal({
|
||||
socket,
|
||||
@ -22,13 +22,12 @@ export default function EditorTerminal({
|
||||
setTerm: (term: Terminal) => void
|
||||
visible: boolean
|
||||
}) {
|
||||
const { theme } = useTheme()
|
||||
const { resolvedTheme: theme } = useTheme()
|
||||
const terminalContainerRef = useRef<ElementRef<"div">>(null)
|
||||
const fitAddonRef = useRef<FitAddon | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!terminalContainerRef.current) return
|
||||
// console.log("new terminal", id, term ? "reusing" : "creating");
|
||||
|
||||
const terminal = new Terminal({
|
||||
cursorBlink: true,
|
||||
@ -37,13 +36,54 @@ export default function EditorTerminal({
|
||||
fontSize: 14,
|
||||
lineHeight: 1.5,
|
||||
letterSpacing: 0,
|
||||
allowTransparency: true,
|
||||
rightClickSelectsWord: true,
|
||||
allowProposedApi: true, // for custom key events
|
||||
})
|
||||
|
||||
// right-click paste handler
|
||||
const handleContextMenu = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
navigator.clipboard.readText().then((text) => {
|
||||
if (text) {
|
||||
socket.emit("terminalData", { id, data: text })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
terminalContainerRef.current.addEventListener(
|
||||
"contextmenu",
|
||||
handleContextMenu
|
||||
)
|
||||
|
||||
// keyboard paste handler
|
||||
terminal.attachCustomKeyEventHandler((event: KeyboardEvent) => {
|
||||
if (event.type === "keydown") {
|
||||
if (
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
event.key.toLowerCase() === "v"
|
||||
) {
|
||||
event.preventDefault()
|
||||
navigator.clipboard.readText().then((text) => {
|
||||
if (text) {
|
||||
socket.emit("terminalData", { id, data: text })
|
||||
}
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
setTerm(terminal)
|
||||
const dispose = () => {
|
||||
|
||||
return () => {
|
||||
terminal.dispose()
|
||||
terminalContainerRef.current?.removeEventListener(
|
||||
"contextmenu",
|
||||
handleContextMenu
|
||||
)
|
||||
}
|
||||
return dispose
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@ -81,7 +121,6 @@ export default function EditorTerminal({
|
||||
|
||||
const { width, height } = entry.contentRect
|
||||
|
||||
// Only call fit if the size has actually changed
|
||||
if (
|
||||
width !== terminalContainerRef.current.offsetWidth ||
|
||||
height !== terminalContainerRef.current.offsetHeight
|
||||
@ -92,10 +131,9 @@ export default function EditorTerminal({
|
||||
console.error("Error during fit:", err)
|
||||
}
|
||||
}
|
||||
}, 50) // Debounce for 50ms
|
||||
}, 50)
|
||||
)
|
||||
|
||||
// start observing for resize
|
||||
resizeObserver.observe(terminalContainerRef.current)
|
||||
return () => {
|
||||
disposableOnData.dispose()
|
||||
@ -124,6 +162,7 @@ export default function EditorTerminal({
|
||||
ref={terminalContainerRef}
|
||||
style={{ display: visible ? "block" : "none" }}
|
||||
className="w-full h-full text-left"
|
||||
tabIndex={0}
|
||||
>
|
||||
{term === null ? (
|
||||
<div className="flex items-center text-muted-foreground p-2">
|
||||
|
@ -9,19 +9,38 @@ import {
|
||||
CardDescription,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/components/ui/hover-card"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { deleteSandbox, updateSandbox, updateUser } from "@/lib/actions"
|
||||
import { socialIcons } from "@/lib/data"
|
||||
import { editUserSchema, EditUserSchema } from "@/lib/schema"
|
||||
import { TIERS } from "@/lib/tiers"
|
||||
import { SandboxWithLiked, User } from "@/lib/types"
|
||||
import { SandboxWithLiked, User, UserLink } from "@/lib/types"
|
||||
import { cn, parseSocialLink } from "@/lib/utils"
|
||||
import { useUser } from "@clerk/nextjs"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import {
|
||||
Edit,
|
||||
Globe,
|
||||
Heart,
|
||||
Info,
|
||||
Loader2,
|
||||
@ -29,17 +48,27 @@ import {
|
||||
Package2,
|
||||
PlusCircle,
|
||||
Sparkles,
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Fragment, useCallback, useEffect, useMemo, useState } from "react"
|
||||
import {
|
||||
Fragment,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useTransition,
|
||||
} from "react"
|
||||
import { useFormState, useFormStatus } from "react-dom"
|
||||
import { useFieldArray, useForm } from "react-hook-form"
|
||||
import { toast } from "sonner"
|
||||
import Avatar from "../ui/avatar"
|
||||
import { Badge } from "../ui/badge"
|
||||
import { Input } from "../ui/input"
|
||||
import { Progress } from "../ui/progress"
|
||||
|
||||
import { Textarea } from "../ui/textarea"
|
||||
// #region Profile Page
|
||||
export default function ProfilePage({
|
||||
publicSandboxes,
|
||||
@ -75,6 +104,9 @@ export default function ProfilePage({
|
||||
generations={isOwnProfile ? loggedInUser.generations : undefined}
|
||||
isOwnProfile={isOwnProfile}
|
||||
tier={profileOwner.tier}
|
||||
bio={profileOwner.bio}
|
||||
personalWebsite={profileOwner.personalWebsite}
|
||||
socialLinks={profileOwner.links}
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
@ -101,11 +133,17 @@ function ProfileCard({
|
||||
joinedDate,
|
||||
generations,
|
||||
isOwnProfile,
|
||||
bio,
|
||||
personalWebsite,
|
||||
socialLinks = [],
|
||||
tier,
|
||||
}: {
|
||||
name: string
|
||||
username: string
|
||||
avatarUrl: string | null
|
||||
bio: string | null
|
||||
personalWebsite: string | null
|
||||
socialLinks: UserLink[]
|
||||
sandboxes: SandboxWithLiked[]
|
||||
joinedDate: Date
|
||||
generations?: number
|
||||
@ -113,9 +151,8 @@ function ProfileCard({
|
||||
tier: string
|
||||
}) {
|
||||
const { user } = useUser()
|
||||
const router = useRouter()
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [formState, formAction] = useFormState(updateUser, {})
|
||||
|
||||
const joinedAt = useMemo(() => {
|
||||
const date = new Date(joinedDate).toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
@ -140,100 +177,126 @@ function ProfileCard({
|
||||
}
|
||||
}, [sandboxes])
|
||||
|
||||
useEffect(() => {
|
||||
if ("message" in formState) {
|
||||
toast.success(formState.message as String)
|
||||
toggleEdit()
|
||||
if ("newRoute" in formState && typeof formState.newRoute === "string") {
|
||||
router.replace(formState.newRoute)
|
||||
}
|
||||
}
|
||||
if ("error" in formState) {
|
||||
const error = formState.error
|
||||
if (typeof error === "string") {
|
||||
toast.error(error)
|
||||
} else {
|
||||
toast.error("An Error Occured")
|
||||
}
|
||||
}
|
||||
}, [formState])
|
||||
const showAddMoreInfoBanner = useMemo(() => {
|
||||
return !bio && !personalWebsite && (socialLinks?.length ?? 0) === 0
|
||||
}, [personalWebsite, bio, socialLinks])
|
||||
|
||||
return (
|
||||
<Card className="mb-6 md:mb-0 sticky top-6">
|
||||
{isOwnProfile && (
|
||||
<Button
|
||||
onClick={toggleEdit}
|
||||
aria-label={isEditing ? "close edit form" : "open edit form"}
|
||||
size="smIcon"
|
||||
variant="secondary"
|
||||
className="rounded-full absolute top-2 right-2"
|
||||
>
|
||||
{isEditing ? <X className="size-4" /> : <Edit className="size-4" />}
|
||||
</Button>
|
||||
<div className="absolute top-2 right-2 flex flex-col gap-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
onClick={toggleEdit}
|
||||
aria-label={isEditing ? "close edit form" : "open edit form"}
|
||||
size="smIcon"
|
||||
variant="secondary"
|
||||
className="rounded-full relative"
|
||||
>
|
||||
{isEditing ? (
|
||||
<X className="size-4" />
|
||||
) : showAddMoreInfoBanner ? (
|
||||
<>
|
||||
<Sparkles className="size-4 text-yellow-400 z-[2]" />
|
||||
<div className="z-[1] absolute inset-0 rounded-full bg-secondary animate-ping" />
|
||||
</>
|
||||
) : (
|
||||
<Edit className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{showAddMoreInfoBanner
|
||||
? "Add more information to your profile"
|
||||
: "Edit your profile"}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
)}
|
||||
<CardContent className="flex flex-col gap-4 items-center pt-6">
|
||||
<Avatar name={name} avatarUrl={avatarUrl} className="size-36" />
|
||||
|
||||
{!isEditing ? (
|
||||
<div className="space-y-2">
|
||||
<CardTitle className="text-2xl text-center">{name}</CardTitle>
|
||||
<CardDescription className="text-center">{`@${username}`}</CardDescription>
|
||||
<CardContent className="flex flex-col gap-4 pt-6">
|
||||
{isEditing ? (
|
||||
<div className="flex flex-col gap-2 items-center ">
|
||||
<Avatar name={name} avatarUrl={avatarUrl} className="size-36" />
|
||||
<EditProfileForm
|
||||
{...{
|
||||
name,
|
||||
username,
|
||||
avatarUrl,
|
||||
bio,
|
||||
personalWebsite,
|
||||
socialLinks,
|
||||
toggleEdit,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<form action={formAction} className="flex flex-col gap-2">
|
||||
<Input
|
||||
name="id"
|
||||
placeholder="ID"
|
||||
className="hidden "
|
||||
value={user?.id}
|
||||
/>
|
||||
<Input
|
||||
name="oldUsername"
|
||||
placeholder="ID"
|
||||
className="hidden "
|
||||
value={user?.username ?? undefined}
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="input-name">Name</Label>
|
||||
<Input
|
||||
id="input-name"
|
||||
name="name"
|
||||
placeholder="Name"
|
||||
defaultValue={name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="input-username">User name</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="input-username"
|
||||
className="peer ps-6"
|
||||
type="text"
|
||||
name="username"
|
||||
placeholder="Username"
|
||||
defaultValue={username}
|
||||
/>
|
||||
<span className="pointer-events-none absolute inset-y-0 start-0 flex items-center justify-center ps-2 text-sm text-muted-foreground peer-disabled:opacity-50">
|
||||
@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SubmitButton />
|
||||
</form>
|
||||
)}
|
||||
{!isEditing && (
|
||||
<>
|
||||
<div className="flex gap-6">
|
||||
<StatsItem icon={Package2} label={stats.sandboxes} />
|
||||
<StatsItem icon={Heart} label={stats.likes} />
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<p className="text-xs text-muted-foreground">{joinedAt}</p>
|
||||
{typeof generations === "number" && (
|
||||
<SubscriptionBadge generations={generations} tier={tier as keyof typeof TIERS} />
|
||||
<div className="flex flex-col gap-2 items-center">
|
||||
<Avatar name={name} avatarUrl={avatarUrl} className="size-36" />
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-2xl text-center">{name}</CardTitle>
|
||||
<CardDescription className="text-center">{`@${username}`}</CardDescription>
|
||||
</div>
|
||||
{bio && <p className="text-sm text-center">{bio}</p>}
|
||||
{(socialLinks.length > 0 || personalWebsite) && (
|
||||
<div className="flex gap-2 justify-center">
|
||||
{personalWebsite && (
|
||||
<Button variant="secondary" size="smIcon" asChild>
|
||||
<a
|
||||
href={personalWebsite}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Globe className="size-4" />
|
||||
<span className="sr-only">Personal Website</span>
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
{socialLinks.map((link, index) => {
|
||||
const Icon = socialIcons[link.platform]
|
||||
return (
|
||||
<Button
|
||||
key={index}
|
||||
variant="secondary"
|
||||
size="smIcon"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
<span className="sr-only">{link.platform}</span>
|
||||
</a>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 items-center">
|
||||
{typeof generations === "number" && (
|
||||
<div className="flex justify-center">
|
||||
<SubscriptionBadge
|
||||
generations={generations}
|
||||
tier={tier as keyof typeof TIERS}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-4">
|
||||
<StatsItem icon={Package2} label={stats.sandboxes} />
|
||||
<StatsItem icon={Heart} label={stats.likes} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs mt-2 text-muted-foreground text-center">
|
||||
{joinedAt}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
@ -241,13 +304,213 @@ function ProfileCard({
|
||||
)
|
||||
}
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending } = useFormStatus()
|
||||
|
||||
function EditProfileForm(props: {
|
||||
name: string
|
||||
username: string
|
||||
avatarUrl: string | null
|
||||
bio: string | null
|
||||
personalWebsite: string | null
|
||||
socialLinks: UserLink[]
|
||||
toggleEdit: () => void
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const { user } = useUser()
|
||||
const formRef = useRef<HTMLFormElement>(null)
|
||||
const [formState, formAction] = useFormState(updateUser, {
|
||||
message: "",
|
||||
})
|
||||
const [isPending, startTransition] = useTransition()
|
||||
const { name, username, bio, personalWebsite, socialLinks, toggleEdit } =
|
||||
props
|
||||
const form = useForm<EditUserSchema>({
|
||||
resolver: zodResolver(editUserSchema),
|
||||
defaultValues: {
|
||||
oldUsername: username,
|
||||
id: user?.id,
|
||||
name,
|
||||
username,
|
||||
bio: bio ?? "",
|
||||
personalWebsite: personalWebsite ?? "",
|
||||
links:
|
||||
socialLinks.length > 0
|
||||
? socialLinks
|
||||
: [{ url: "", platform: "generic" }],
|
||||
...(formState.fields ?? {}),
|
||||
},
|
||||
})
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: "links",
|
||||
control: form.control,
|
||||
})
|
||||
useEffect(() => {
|
||||
const message = formState.message
|
||||
if (!Boolean(message)) return
|
||||
if ("error" in formState) {
|
||||
toast.error(formState.message)
|
||||
return
|
||||
}
|
||||
toast.success(formState.message as String)
|
||||
toggleEdit()
|
||||
if (formState?.newRoute) {
|
||||
router.replace(formState.newRoute)
|
||||
}
|
||||
}, [formState])
|
||||
return (
|
||||
<Button size="sm" type="submit" className="w-full mt-2" disabled={pending}>
|
||||
{pending && <Loader2 className="animate-spin mr-2 h-4 w-4" />}
|
||||
Save
|
||||
<Form {...form}>
|
||||
<form
|
||||
ref={formRef}
|
||||
action={formAction}
|
||||
onSubmit={(evt) => {
|
||||
evt.preventDefault()
|
||||
form.handleSubmit(() => {
|
||||
startTransition(() => {
|
||||
formAction(new FormData(formRef.current!))
|
||||
})
|
||||
})(evt)
|
||||
}}
|
||||
className="space-y-3 w-full"
|
||||
>
|
||||
<input type="hidden" name="id" value={user?.id} />
|
||||
<input type="hidden" name="oldUsername" value={username} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="marie doe" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>User name</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Input
|
||||
className="peer ps-6"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
{...field}
|
||||
/>
|
||||
<span className="pointer-events-none absolute inset-y-0 start-0 flex items-center justify-center ps-2 text-sm text-muted-foreground peer-disabled:opacity-50">
|
||||
@
|
||||
</span>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bio"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Bio</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="hi, I love building things!"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="personalWebsite"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Personal Website</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://chillguy.dev" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
<FormField
|
||||
control={form.control}
|
||||
key={field.id}
|
||||
name={`links.${index}`}
|
||||
render={({ field: { onChange, value, ...field } }) => {
|
||||
const Icon = socialIcons[value.platform] ?? socialIcons.generic
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel className={cn(index !== 0 && "sr-only")}>
|
||||
Social Links
|
||||
</FormLabel>
|
||||
<FormDescription className={cn(index !== 0 && "sr-only")}>
|
||||
Add links to your blogs or social media profiles.
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
{...field}
|
||||
className="peer ps-9"
|
||||
value={value.url}
|
||||
onChange={(e) =>
|
||||
onChange(parseSocialLink(e.currentTarget.value))
|
||||
}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-y-0 start-0 flex items-center justify-center ps-3 text-muted-foreground/80 peer-disabled:opacity-50">
|
||||
<Icon
|
||||
size={16}
|
||||
strokeWidth={2}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="smIcon"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-2"
|
||||
onClick={() => append({ url: "", platform: "generic" })}
|
||||
>
|
||||
Add URL
|
||||
</Button>
|
||||
</div>
|
||||
<SubmitButton {...{ isPending }} />
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
function SubmitButton({ isPending }: { isPending: boolean }) {
|
||||
const formStatus = useFormStatus()
|
||||
const { pending } = formStatus
|
||||
const pend = pending || isPending
|
||||
return (
|
||||
<Button size="sm" type="submit" className="w-full mt-2" disabled={pend}>
|
||||
{pend && <Loader2 className="animate-spin mr-2 h-4 w-4" />}
|
||||
Save Changes
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@ -438,14 +701,20 @@ interface StatsItemProps {
|
||||
|
||||
const StatsItem = ({ icon: Icon, label }: StatsItemProps) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon size={18} />
|
||||
<span className="text-sm text-muted-foreground">{label}</span>
|
||||
<Icon size={16} />
|
||||
<span className="text-sm text-muted-foreground">{label}</span>
|
||||
</div>
|
||||
)
|
||||
// #endregion
|
||||
|
||||
// #region Sub Badge
|
||||
const SubscriptionBadge = ({ generations, tier = "FREE" }: { generations: number, tier?: keyof typeof TIERS }) => {
|
||||
const SubscriptionBadge = ({
|
||||
generations,
|
||||
tier = "FREE",
|
||||
}: {
|
||||
generations: number
|
||||
tier?: keyof typeof TIERS
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex gap-2 items-center">
|
||||
<Badge variant="secondary" className="text-sm cursor-pointer">
|
||||
@ -453,8 +722,8 @@ const SubscriptionBadge = ({ generations, tier = "FREE" }: { generations: number
|
||||
</Badge>
|
||||
<HoverCard>
|
||||
<HoverCardTrigger>
|
||||
<Button variant="ghost" size="smIcon">
|
||||
<Info size={20} />
|
||||
<Button variant="ghost" size="smIcon" className="size-[26px]">
|
||||
<Info size={16} />
|
||||
</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
|
@ -9,14 +9,19 @@ import { Button } from "../ui/button"
|
||||
export default function ProfileNavbar({ userData }: { userData: User }) {
|
||||
return (
|
||||
<nav className=" py-2 px-4 w-full flex items-center justify-between border-b border-border">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link
|
||||
href="/"
|
||||
className="ring-offset-2 ring-offset-background focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none rounded-sm"
|
||||
>
|
||||
<Image src={Logo} alt="Logo" width={36} height={36} />
|
||||
</Link>
|
||||
<div className="text-sm font-medium flex items-center">Sandbox</div>
|
||||
<h1 className="text-xl">
|
||||
<span className="font-semibold">Sandbox</span>{" "}
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
by gitwit
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<ThemeSwitcher />
|
||||
|
@ -26,7 +26,7 @@ const buttonVariants = cva(
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
smIcon: "h-8 w-8",
|
||||
smIcon: "size-8",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
@ -1,6 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import * as React from "react"
|
||||
import {
|
||||
Controller,
|
||||
ControllerProps,
|
||||
@ -10,8 +12,8 @@ import {
|
||||
useFormContext,
|
||||
} from "react-hook-form"
|
||||
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
@ -93,7 +95,7 @@ const FormLabel = React.forwardRef<
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(className)}
|
||||
className={cn(error && "text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
@ -165,12 +167,12 @@ const FormMessage = React.forwardRef<
|
||||
FormMessage.displayName = "FormMessage"
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
useFormField,
|
||||
FormField,
|
||||
}
|
||||
|
48
frontend/components/ui/scroll-area.tsx
Normal file
48
frontend/components/ui/scroll-area.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
22
frontend/components/ui/textarea.tsx
Normal file
22
frontend/components/ui/textarea.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Textarea = React.forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
React.ComponentProps<"textarea">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
@ -1,8 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
import { type ThemeProviderProps } from "next-themes/dist/types"
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@ -29,4 +29,4 @@ const TooltipContent = React.forwardRef<
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
import { User } from "@/lib/types"
|
||||
import { useClerk } from "@clerk/nextjs"
|
||||
import {
|
||||
Crown,
|
||||
Crown,
|
||||
LayoutDashboard,
|
||||
LogOut,
|
||||
Sparkles,
|
||||
@ -43,7 +43,11 @@ const TIER_INFO = {
|
||||
},
|
||||
} as const
|
||||
|
||||
export default function UserButton({ userData: initialUserData }: { userData: User }) {
|
||||
export default function UserButton({
|
||||
userData: initialUserData,
|
||||
}: {
|
||||
userData: User
|
||||
}) {
|
||||
const [userData, setUserData] = useState<User>(initialUserData)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const { signOut } = useClerk()
|
||||
@ -57,7 +61,7 @@ export default function UserButton({ userData: initialUserData }: { userData: Us
|
||||
headers: {
|
||||
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
|
||||
},
|
||||
cache: 'no-store'
|
||||
cache: "no-store",
|
||||
}
|
||||
)
|
||||
if (res.ok) {
|
||||
@ -75,9 +79,12 @@ export default function UserButton({ userData: initialUserData }: { userData: Us
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const tierInfo = TIER_INFO[userData.tier as keyof typeof TIER_INFO] || TIER_INFO.FREE
|
||||
const tierInfo =
|
||||
TIER_INFO[userData.tier as keyof typeof TIER_INFO] || TIER_INFO.FREE
|
||||
const TierIcon = tierInfo.icon
|
||||
const usagePercentage = Math.floor((userData.generations || 0) * 100 / tierInfo.limit)
|
||||
const usagePercentage = Math.floor(
|
||||
((userData.generations || 0) * 100) / tierInfo.limit
|
||||
)
|
||||
|
||||
const handleUpgrade = async () => {
|
||||
router.push(`/@${userData.username}`)
|
||||
@ -98,7 +105,6 @@ export default function UserButton({ userData: initialUserData }: { userData: Us
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
|
||||
<DropdownMenuItem className="cursor-pointer" asChild>
|
||||
<Link href={"/dashboard"}>
|
||||
<LayoutDashboard className="mr-2 size-4" />
|
||||
@ -114,12 +120,13 @@ export default function UserButton({ userData: initialUserData }: { userData: Us
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
|
||||
<div className="py-1.5 px-2 w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<TierIcon className={`h-4 w-4 ${tierInfo.color}`} />
|
||||
<span className="text-sm font-medium">{userData.tier || "FREE"} Plan</span>
|
||||
<span className="text-sm font-medium">
|
||||
{userData.tier || "FREE"} Plan
|
||||
</span>
|
||||
</div>
|
||||
{(userData.tier === "FREE" || userData.tier === "PRO") && (
|
||||
<Button
|
||||
@ -139,16 +146,20 @@ export default function UserButton({ userData: initialUserData }: { userData: Us
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground mb-2">
|
||||
<span>AI Usage</span>
|
||||
<span>{userData.generations}/{tierInfo.limit}</span>
|
||||
<span>
|
||||
{userData.generations}/{tierInfo.limit}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="rounded-full w-full h-2 overflow-hidden bg-secondary mb-1">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-300 ${
|
||||
usagePercentage > 90 ? 'bg-red-500' :
|
||||
usagePercentage > 75 ? 'bg-yellow-500' :
|
||||
tierInfo.color.replace('text-', 'bg-')
|
||||
}`}
|
||||
usagePercentage > 90
|
||||
? "bg-red-500"
|
||||
: usagePercentage > 75
|
||||
? "bg-yellow-500"
|
||||
: tierInfo.color.replace("text-", "bg-")
|
||||
}`}
|
||||
style={{
|
||||
width: `${Math.min(usagePercentage, 100)}%`,
|
||||
}}
|
||||
@ -173,4 +184,3 @@ export default function UserButton({ userData: initialUserData }: { userData: Us
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,9 @@ interface TerminalContextType {
|
||||
createNewTerminal: (command?: string) => Promise<void>
|
||||
closeTerminal: (id: string) => void
|
||||
deploy: (callback: () => void) => void
|
||||
getAppExists:
|
||||
| ((appName: string) => Promise<{ success: boolean; exists?: boolean }>)
|
||||
| null
|
||||
}
|
||||
|
||||
const TerminalContext = createContext<TerminalContextType | undefined>(
|
||||
@ -35,6 +38,19 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
>([])
|
||||
const [activeTerminalId, setActiveTerminalId] = useState<string>("")
|
||||
const [creatingTerminal, setCreatingTerminal] = useState<boolean>(false)
|
||||
const [isSocketReady, setIsSocketReady] = useState<boolean>(false)
|
||||
|
||||
// Listen for the "ready" signal from the socket
|
||||
React.useEffect(() => {
|
||||
if (socket) {
|
||||
socket.on("ready", () => {
|
||||
setIsSocketReady(true)
|
||||
})
|
||||
}
|
||||
return () => {
|
||||
if (socket) socket.off("ready")
|
||||
}
|
||||
}, [socket])
|
||||
|
||||
const createNewTerminal = async (command?: string): Promise<void> => {
|
||||
if (!socket) return
|
||||
@ -78,6 +94,20 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
})
|
||||
}
|
||||
|
||||
const getAppExists = async (
|
||||
appName: string
|
||||
): Promise<{ success: boolean; exists?: boolean }> => {
|
||||
console.log("Is there a socket: " + !!socket)
|
||||
if (!socket) {
|
||||
console.error("Couldn't check if app exists: No socket")
|
||||
return { success: false }
|
||||
}
|
||||
const response: { success: boolean; exists?: boolean } = await new Promise(
|
||||
(resolve) => socket.emit("getAppExists", { appName }, resolve)
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
const value = {
|
||||
terminals,
|
||||
setTerminals,
|
||||
@ -88,6 +118,7 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
createNewTerminal,
|
||||
closeTerminal,
|
||||
deploy,
|
||||
getAppExists: isSocketReady ? getAppExists : null,
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -2,6 +2,9 @@
|
||||
|
||||
import { revalidatePath } from "next/cache"
|
||||
import { z } from "zod"
|
||||
import { editUserSchema } from "./schema"
|
||||
import { UserLink } from "./types"
|
||||
import { parseSocialLink } from "./utils"
|
||||
|
||||
export async function createSandbox(body: {
|
||||
type: string
|
||||
@ -94,7 +97,7 @@ export async function unshareSandbox(sandboxId: string, userId: string) {
|
||||
}
|
||||
|
||||
export async function toggleLike(sandboxId: string, userId: string) {
|
||||
await fetch(
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/sandbox/like`,
|
||||
{
|
||||
method: "POST",
|
||||
@ -123,20 +126,31 @@ const UpdateErrorSchema = z.object({
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export async function updateUser(prevState: any, formData: FormData) {
|
||||
const data = Object.fromEntries(formData)
|
||||
|
||||
const schema = z.object({
|
||||
id: z.string(),
|
||||
username: z.string(),
|
||||
oldUsername: z.string(),
|
||||
name: z.string(),
|
||||
interface FormState {
|
||||
message: string
|
||||
error?: any
|
||||
newRoute?: string
|
||||
fields?: Record<string, unknown>
|
||||
}
|
||||
export async function updateUser(
|
||||
prevState: any,
|
||||
formData: FormData
|
||||
): Promise<FormState> {
|
||||
let data = Object.fromEntries(formData)
|
||||
let links: UserLink[] = []
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (key.startsWith("link")) {
|
||||
const [_, index] = key.split(".")
|
||||
if (value) {
|
||||
links.splice(parseInt(index), 0, parseSocialLink(value as string))
|
||||
delete data[key]
|
||||
}
|
||||
}
|
||||
})
|
||||
console.log(data)
|
||||
|
||||
// @ts-ignore
|
||||
data.links = links
|
||||
try {
|
||||
const validatedData = schema.parse(data)
|
||||
|
||||
const validatedData = editUserSchema.parse(data)
|
||||
const changedUsername = validatedData.username !== validatedData.oldUsername
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user`,
|
||||
@ -150,6 +164,9 @@ export async function updateUser(prevState: any, formData: FormData) {
|
||||
id: validatedData.id,
|
||||
username: data.username ?? undefined,
|
||||
name: data.name ?? undefined,
|
||||
bio: data.bio ?? undefined,
|
||||
personalWebsite: data.personalWebsite ?? undefined,
|
||||
links: data.links ?? undefined,
|
||||
}),
|
||||
}
|
||||
)
|
||||
@ -160,11 +177,11 @@ export async function updateUser(prevState: any, formData: FormData) {
|
||||
const parseResult = UpdateErrorSchema.safeParse(responseData)
|
||||
|
||||
if (!parseResult.success) {
|
||||
return { error: "Unexpected error occurred" }
|
||||
}
|
||||
|
||||
if (parseResult.data.error) {
|
||||
return parseResult.data
|
||||
return {
|
||||
message: "Unexpected error occurred",
|
||||
error: parseResult.error,
|
||||
fields: validatedData,
|
||||
}
|
||||
}
|
||||
|
||||
if (changedUsername) {
|
||||
@ -175,12 +192,13 @@ export async function updateUser(prevState: any, formData: FormData) {
|
||||
return { message: "Successfully updated" }
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
console.log(error)
|
||||
return {
|
||||
error: error.errors?.[0].message,
|
||||
message: "Invalid data",
|
||||
error: error.errors,
|
||||
fields: data,
|
||||
}
|
||||
}
|
||||
|
||||
return { error: "An unexpected error occurred" }
|
||||
return { message: "An unexpected error occurred", fields: data }
|
||||
}
|
||||
}
|
||||
|
14
frontend/lib/constants/index.ts
Normal file
14
frontend/lib/constants/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export const KNOWN_PLATFORMS = [
|
||||
"github",
|
||||
"twitter",
|
||||
"instagram",
|
||||
"bluesky",
|
||||
"linkedin",
|
||||
"youtube",
|
||||
"twitch",
|
||||
"discord",
|
||||
"mastodon",
|
||||
"threads",
|
||||
"gitlab",
|
||||
"generic",
|
||||
] as const
|
@ -1,3 +1,37 @@
|
||||
import {
|
||||
AtSign,
|
||||
Github,
|
||||
GitlabIcon as GitlabLogo,
|
||||
Globe,
|
||||
Instagram,
|
||||
Link,
|
||||
Linkedin,
|
||||
MessageCircle,
|
||||
Twitch,
|
||||
Twitter,
|
||||
Youtube,
|
||||
} from "lucide-react"
|
||||
import { KnownPlatform } from "../types"
|
||||
|
||||
export const socialIcons: Record<
|
||||
KnownPlatform | "website",
|
||||
React.ComponentType<any>
|
||||
> = {
|
||||
github: Github,
|
||||
twitter: Twitter,
|
||||
instagram: Instagram,
|
||||
bluesky: AtSign,
|
||||
linkedin: Linkedin,
|
||||
youtube: Youtube,
|
||||
twitch: Twitch,
|
||||
discord: MessageCircle,
|
||||
mastodon: AtSign,
|
||||
threads: AtSign,
|
||||
gitlab: GitlabLogo,
|
||||
generic: Link,
|
||||
website: Globe,
|
||||
}
|
||||
|
||||
export const projectTemplates: {
|
||||
id: string
|
||||
name: string
|
||||
@ -38,6 +72,6 @@ export const projectTemplates: {
|
||||
name: "PHP",
|
||||
description: "PHP development environment",
|
||||
icon: "/project-icons/php.svg",
|
||||
disabled: false
|
||||
disabled: false,
|
||||
},
|
||||
]
|
||||
|
20
frontend/lib/schema/index.ts
Normal file
20
frontend/lib/schema/index.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { z } from "zod"
|
||||
import { KNOWN_PLATFORMS } from "../constants"
|
||||
|
||||
export const editUserSchema = z.object({
|
||||
id: z.string().trim(),
|
||||
username: z.string().trim().min(1, "Username must be at least 1 character"),
|
||||
oldUsername: z.string().trim(),
|
||||
name: z.string().trim().min(1, "Name must be at least 1 character"),
|
||||
bio: z.string().trim().optional(),
|
||||
personalWebsite: z.string().trim().optional(),
|
||||
links: z
|
||||
.array(
|
||||
z.object({
|
||||
url: z.string().trim(),
|
||||
platform: z.enum(KNOWN_PLATFORMS),
|
||||
})
|
||||
)
|
||||
.catch([]),
|
||||
})
|
||||
export type EditUserSchema = z.infer<typeof editUserSchema>
|
@ -1,290 +1,290 @@
|
||||
export interface TemplateConfig {
|
||||
id: string
|
||||
name: string,
|
||||
runCommand: string,
|
||||
fileStructure: {
|
||||
[key: string]: {
|
||||
purpose: string
|
||||
description: string
|
||||
}
|
||||
}
|
||||
conventions: string[]
|
||||
dependencies?: {
|
||||
[key: string]: string
|
||||
}
|
||||
scripts?: {
|
||||
[key: string]: string
|
||||
id: string
|
||||
name: string
|
||||
runCommand: string
|
||||
fileStructure: {
|
||||
[key: string]: {
|
||||
purpose: string
|
||||
description: string
|
||||
}
|
||||
}
|
||||
|
||||
export const templateConfigs: { [key: string]: TemplateConfig } = {
|
||||
reactjs: {
|
||||
id: "reactjs",
|
||||
name: "React",
|
||||
runCommand: "npm run dev",
|
||||
fileStructure: {
|
||||
"src/": {
|
||||
purpose: "source",
|
||||
description: "Contains all React components and application logic"
|
||||
},
|
||||
"src/components/": {
|
||||
purpose: "components",
|
||||
description: "Reusable React components"
|
||||
},
|
||||
"src/lib/": {
|
||||
purpose: "utilities",
|
||||
description: "Utility functions and shared code"
|
||||
},
|
||||
"src/App.tsx": {
|
||||
purpose: "entry",
|
||||
description: "Main application component"
|
||||
},
|
||||
"src/index.tsx": {
|
||||
purpose: "entry",
|
||||
description: "Application entry point"
|
||||
},
|
||||
"src/index.css": {
|
||||
purpose: "styles",
|
||||
description: "Global CSS styles"
|
||||
},
|
||||
"public/": {
|
||||
purpose: "static",
|
||||
description: "Static assets and index.html"
|
||||
},
|
||||
"tsconfig.json": {
|
||||
purpose: "config",
|
||||
description: "TypeScript configuration"
|
||||
},
|
||||
"vite.config.ts": {
|
||||
purpose: "config",
|
||||
description: "Vite bundler configuration"
|
||||
},
|
||||
"package.json": {
|
||||
purpose: "config",
|
||||
description: "Project dependencies and scripts"
|
||||
}
|
||||
},
|
||||
conventions: [
|
||||
"Use functional components with hooks",
|
||||
"Follow React naming conventions (PascalCase for components)",
|
||||
"Keep components small and focused",
|
||||
"Use TypeScript for type safety"
|
||||
],
|
||||
dependencies: {
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.441.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
scripts: {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
}
|
||||
},
|
||||
// Next.js template config
|
||||
nextjs: {
|
||||
id: "nextjs",
|
||||
name: "NextJS",
|
||||
runCommand: "npm run dev",
|
||||
fileStructure: {
|
||||
"pages/": {
|
||||
purpose: "routing",
|
||||
description: "Page components and API routes"
|
||||
},
|
||||
"pages/api/": {
|
||||
purpose: "api",
|
||||
description: "API route handlers"
|
||||
},
|
||||
"pages/_app.tsx": {
|
||||
purpose: "entry",
|
||||
description: "Application wrapper component"
|
||||
},
|
||||
"pages/index.tsx": {
|
||||
purpose: "page",
|
||||
description: "Homepage component"
|
||||
},
|
||||
"public/": {
|
||||
purpose: "static",
|
||||
description: "Static assets and files"
|
||||
},
|
||||
"styles/": {
|
||||
purpose: "styles",
|
||||
description: "CSS modules and global styles"
|
||||
},
|
||||
"styles/globals.css": {
|
||||
purpose: "styles",
|
||||
description: "Global CSS styles"
|
||||
},
|
||||
"styles/Home.module.css": {
|
||||
purpose: "styles",
|
||||
description: "Homepage-specific styles"
|
||||
},
|
||||
"next.config.js": {
|
||||
purpose: "config",
|
||||
description: "Next.js configuration"
|
||||
},
|
||||
"next-env.d.ts": {
|
||||
purpose: "types",
|
||||
description: "Next.js TypeScript declarations"
|
||||
},
|
||||
"tsconfig.json": {
|
||||
purpose: "config",
|
||||
description: "TypeScript configuration"
|
||||
},
|
||||
"package.json": {
|
||||
purpose: "config",
|
||||
description: "Project dependencies and scripts"
|
||||
}
|
||||
},
|
||||
conventions: [
|
||||
"Use file-system based routing",
|
||||
"Keep API routes in pages/api",
|
||||
"Use CSS Modules for component styles",
|
||||
"Follow Next.js data fetching patterns"
|
||||
],
|
||||
dependencies: {
|
||||
"next": "^14.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"tailwindcss": "^3.4.1"
|
||||
},
|
||||
scripts: {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
}
|
||||
},
|
||||
// Streamlit template config
|
||||
streamlit: {
|
||||
id: "streamlit",
|
||||
name: "Streamlit",
|
||||
runCommand: "./venv/bin/streamlit run main.py --server.runOnSave true",
|
||||
fileStructure: {
|
||||
"main.py": {
|
||||
purpose: "entry",
|
||||
description: "Main Streamlit application file"
|
||||
},
|
||||
"requirements.txt": {
|
||||
purpose: "dependencies",
|
||||
description: "Python package dependencies"
|
||||
},
|
||||
"Procfile": {
|
||||
purpose: "deployment",
|
||||
description: "Deployment configuration for hosting platforms"
|
||||
},
|
||||
"venv/": {
|
||||
purpose: "environment",
|
||||
description: "Python virtual environment directory"
|
||||
}
|
||||
},
|
||||
conventions: [
|
||||
"Use Streamlit components for UI",
|
||||
"Follow PEP 8 style guide",
|
||||
"Keep dependencies in requirements.txt",
|
||||
"Use virtual environment for isolation"
|
||||
],
|
||||
dependencies: {
|
||||
"streamlit": "^1.40.0",
|
||||
"altair": "^5.5.0"
|
||||
},
|
||||
scripts: {
|
||||
"start": "streamlit run main.py",
|
||||
"dev": "./venv/bin/streamlit run main.py --server.runOnSave true"
|
||||
}
|
||||
},
|
||||
// HTML template config
|
||||
vanillajs: {
|
||||
id: "vanillajs",
|
||||
name: "HTML/JS",
|
||||
runCommand: "npm run dev",
|
||||
fileStructure: {
|
||||
"index.html": {
|
||||
purpose: "entry",
|
||||
description: "Main HTML entry point"
|
||||
},
|
||||
"style.css": {
|
||||
purpose: "styles",
|
||||
description: "Global CSS styles"
|
||||
},
|
||||
"script.js": {
|
||||
purpose: "scripts",
|
||||
description: "JavaScript application logic"
|
||||
},
|
||||
"package.json": {
|
||||
purpose: "config",
|
||||
description: "Project dependencies and scripts"
|
||||
},
|
||||
"package-lock.json": {
|
||||
purpose: "config",
|
||||
description: "Locked dependency versions"
|
||||
},
|
||||
"vite.config.js": {
|
||||
purpose: "config",
|
||||
description: "Vite bundler configuration"
|
||||
}
|
||||
},
|
||||
conventions: [
|
||||
"Use semantic HTML elements",
|
||||
"Keep CSS modular and organized",
|
||||
"Write clean, modular JavaScript",
|
||||
"Follow modern ES6+ practices"
|
||||
],
|
||||
dependencies: {
|
||||
"vite": "^5.0.12"
|
||||
},
|
||||
scripts: {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
}
|
||||
},
|
||||
// PHP template config
|
||||
php: {
|
||||
id: "php",
|
||||
name: "PHP",
|
||||
runCommand: "npx vite",
|
||||
fileStructure: {
|
||||
"index.php": {
|
||||
purpose: "entry",
|
||||
description: "Main PHP entry point"
|
||||
},
|
||||
"package.json": {
|
||||
purpose: "config",
|
||||
description: "Frontend dependencies and scripts"
|
||||
},
|
||||
"package-lock.json": {
|
||||
purpose: "config",
|
||||
description: "Locked dependency versions"
|
||||
},
|
||||
"vite.config.js": {
|
||||
purpose: "config",
|
||||
description: "Vite configuration for frontend assets"
|
||||
},
|
||||
"node_modules/": {
|
||||
purpose: "dependencies",
|
||||
description: "Frontend dependency files"
|
||||
}
|
||||
},
|
||||
conventions: [
|
||||
"Follow PSR-12 coding standards",
|
||||
"Use modern PHP 8+ features",
|
||||
"Organize assets with Vite",
|
||||
"Keep PHP logic separate from presentation"
|
||||
],
|
||||
dependencies: {
|
||||
"vite": "^5.0.0"
|
||||
},
|
||||
scripts: {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
}
|
||||
}
|
||||
conventions: string[]
|
||||
dependencies?: {
|
||||
[key: string]: string
|
||||
}
|
||||
scripts?: {
|
||||
[key: string]: string
|
||||
}
|
||||
}
|
||||
|
||||
export const templateConfigs: { [key: string]: TemplateConfig } = {
|
||||
reactjs: {
|
||||
id: "reactjs",
|
||||
name: "React",
|
||||
runCommand: "npm run dev",
|
||||
fileStructure: {
|
||||
"src/": {
|
||||
purpose: "source",
|
||||
description: "Contains all React components and application logic",
|
||||
},
|
||||
"src/components/": {
|
||||
purpose: "components",
|
||||
description: "Reusable React components",
|
||||
},
|
||||
"src/lib/": {
|
||||
purpose: "utilities",
|
||||
description: "Utility functions and shared code",
|
||||
},
|
||||
"src/App.tsx": {
|
||||
purpose: "entry",
|
||||
description: "Main application component",
|
||||
},
|
||||
"src/index.tsx": {
|
||||
purpose: "entry",
|
||||
description: "Application entry point",
|
||||
},
|
||||
"src/index.css": {
|
||||
purpose: "styles",
|
||||
description: "Global CSS styles",
|
||||
},
|
||||
"public/": {
|
||||
purpose: "static",
|
||||
description: "Static assets and index.html",
|
||||
},
|
||||
"tsconfig.json": {
|
||||
purpose: "config",
|
||||
description: "TypeScript configuration",
|
||||
},
|
||||
"vite.config.ts": {
|
||||
purpose: "config",
|
||||
description: "Vite bundler configuration",
|
||||
},
|
||||
"package.json": {
|
||||
purpose: "config",
|
||||
description: "Project dependencies and scripts",
|
||||
},
|
||||
},
|
||||
conventions: [
|
||||
"Use functional components with hooks",
|
||||
"Follow React naming conventions (PascalCase for components)",
|
||||
"Keep components small and focused",
|
||||
"Use TypeScript for type safety",
|
||||
],
|
||||
dependencies: {
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
clsx: "^2.1.1",
|
||||
"lucide-react": "^0.441.0",
|
||||
react: "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
},
|
||||
scripts: {
|
||||
dev: "vite",
|
||||
build: "tsc && vite build",
|
||||
preview: "vite preview",
|
||||
},
|
||||
},
|
||||
// Next.js template config
|
||||
nextjs: {
|
||||
id: "nextjs",
|
||||
name: "NextJS",
|
||||
runCommand: "npm run dev",
|
||||
fileStructure: {
|
||||
"pages/": {
|
||||
purpose: "routing",
|
||||
description: "Page components and API routes",
|
||||
},
|
||||
"pages/api/": {
|
||||
purpose: "api",
|
||||
description: "API route handlers",
|
||||
},
|
||||
"pages/_app.tsx": {
|
||||
purpose: "entry",
|
||||
description: "Application wrapper component",
|
||||
},
|
||||
"pages/index.tsx": {
|
||||
purpose: "page",
|
||||
description: "Homepage component",
|
||||
},
|
||||
"public/": {
|
||||
purpose: "static",
|
||||
description: "Static assets and files",
|
||||
},
|
||||
"styles/": {
|
||||
purpose: "styles",
|
||||
description: "CSS modules and global styles",
|
||||
},
|
||||
"styles/globals.css": {
|
||||
purpose: "styles",
|
||||
description: "Global CSS styles",
|
||||
},
|
||||
"styles/Home.module.css": {
|
||||
purpose: "styles",
|
||||
description: "Homepage-specific styles",
|
||||
},
|
||||
"next.config.js": {
|
||||
purpose: "config",
|
||||
description: "Next.js configuration",
|
||||
},
|
||||
"next-env.d.ts": {
|
||||
purpose: "types",
|
||||
description: "Next.js TypeScript declarations",
|
||||
},
|
||||
"tsconfig.json": {
|
||||
purpose: "config",
|
||||
description: "TypeScript configuration",
|
||||
},
|
||||
"package.json": {
|
||||
purpose: "config",
|
||||
description: "Project dependencies and scripts",
|
||||
},
|
||||
},
|
||||
conventions: [
|
||||
"Use file-system based routing",
|
||||
"Keep API routes in pages/api",
|
||||
"Use CSS Modules for component styles",
|
||||
"Follow Next.js data fetching patterns",
|
||||
],
|
||||
dependencies: {
|
||||
next: "^14.1.0",
|
||||
react: "^18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
tailwindcss: "^3.4.1",
|
||||
},
|
||||
scripts: {
|
||||
dev: "next dev",
|
||||
build: "next build",
|
||||
start: "next start",
|
||||
lint: "next lint",
|
||||
},
|
||||
},
|
||||
// Streamlit template config
|
||||
streamlit: {
|
||||
id: "streamlit",
|
||||
name: "Streamlit",
|
||||
runCommand: "./venv/bin/streamlit run main.py --server.runOnSave true",
|
||||
fileStructure: {
|
||||
"main.py": {
|
||||
purpose: "entry",
|
||||
description: "Main Streamlit application file",
|
||||
},
|
||||
"requirements.txt": {
|
||||
purpose: "dependencies",
|
||||
description: "Python package dependencies",
|
||||
},
|
||||
Procfile: {
|
||||
purpose: "deployment",
|
||||
description: "Deployment configuration for hosting platforms",
|
||||
},
|
||||
"venv/": {
|
||||
purpose: "environment",
|
||||
description: "Python virtual environment directory",
|
||||
},
|
||||
},
|
||||
conventions: [
|
||||
"Use Streamlit components for UI",
|
||||
"Follow PEP 8 style guide",
|
||||
"Keep dependencies in requirements.txt",
|
||||
"Use virtual environment for isolation",
|
||||
],
|
||||
dependencies: {
|
||||
streamlit: "^1.40.0",
|
||||
altair: "^5.5.0",
|
||||
},
|
||||
scripts: {
|
||||
start: "streamlit run main.py",
|
||||
dev: "./venv/bin/streamlit run main.py --server.runOnSave true",
|
||||
},
|
||||
},
|
||||
// HTML template config
|
||||
vanillajs: {
|
||||
id: "vanillajs",
|
||||
name: "HTML/JS",
|
||||
runCommand: "npm run dev",
|
||||
fileStructure: {
|
||||
"index.html": {
|
||||
purpose: "entry",
|
||||
description: "Main HTML entry point",
|
||||
},
|
||||
"style.css": {
|
||||
purpose: "styles",
|
||||
description: "Global CSS styles",
|
||||
},
|
||||
"script.js": {
|
||||
purpose: "scripts",
|
||||
description: "JavaScript application logic",
|
||||
},
|
||||
"package.json": {
|
||||
purpose: "config",
|
||||
description: "Project dependencies and scripts",
|
||||
},
|
||||
"package-lock.json": {
|
||||
purpose: "config",
|
||||
description: "Locked dependency versions",
|
||||
},
|
||||
"vite.config.js": {
|
||||
purpose: "config",
|
||||
description: "Vite bundler configuration",
|
||||
},
|
||||
},
|
||||
conventions: [
|
||||
"Use semantic HTML elements",
|
||||
"Keep CSS modular and organized",
|
||||
"Write clean, modular JavaScript",
|
||||
"Follow modern ES6+ practices",
|
||||
],
|
||||
dependencies: {
|
||||
vite: "^5.0.12",
|
||||
},
|
||||
scripts: {
|
||||
dev: "vite",
|
||||
build: "vite build",
|
||||
preview: "vite preview",
|
||||
},
|
||||
},
|
||||
// PHP template config
|
||||
php: {
|
||||
id: "php",
|
||||
name: "PHP",
|
||||
runCommand: "npx vite",
|
||||
fileStructure: {
|
||||
"index.php": {
|
||||
purpose: "entry",
|
||||
description: "Main PHP entry point",
|
||||
},
|
||||
"package.json": {
|
||||
purpose: "config",
|
||||
description: "Frontend dependencies and scripts",
|
||||
},
|
||||
"package-lock.json": {
|
||||
purpose: "config",
|
||||
description: "Locked dependency versions",
|
||||
},
|
||||
"vite.config.js": {
|
||||
purpose: "config",
|
||||
description: "Vite configuration for frontend assets",
|
||||
},
|
||||
"node_modules/": {
|
||||
purpose: "dependencies",
|
||||
description: "Frontend dependency files",
|
||||
},
|
||||
},
|
||||
conventions: [
|
||||
"Follow PSR-12 coding standards",
|
||||
"Use modern PHP 8+ features",
|
||||
"Organize assets with Vite",
|
||||
"Keep PHP logic separate from presentation",
|
||||
],
|
||||
dependencies: {
|
||||
vite: "^5.0.0",
|
||||
},
|
||||
scripts: {
|
||||
dev: "vite",
|
||||
build: "vite build",
|
||||
preview: "vite preview",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -1,19 +1,19 @@
|
||||
export const TIERS = {
|
||||
FREE: {
|
||||
// generations: 100,
|
||||
// maxTokens: 1024,
|
||||
generations: 1000,
|
||||
maxTokens: 4096,
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
},
|
||||
PRO: {
|
||||
generations: 500,
|
||||
maxTokens: 2048,
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
},
|
||||
ENTERPRISE: {
|
||||
generations: 1000,
|
||||
maxTokens: 4096,
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
},
|
||||
FREE: {
|
||||
// generations: 100,
|
||||
// maxTokens: 1024,
|
||||
generations: 1000,
|
||||
maxTokens: 4096,
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
},
|
||||
PRO: {
|
||||
generations: 500,
|
||||
maxTokens: 2048,
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
},
|
||||
ENTERPRISE: {
|
||||
generations: 1000,
|
||||
maxTokens: 4096,
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
},
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
// DB Types
|
||||
|
||||
import { KNOWN_PLATFORMS } from "./constants"
|
||||
|
||||
export type User = {
|
||||
id: string
|
||||
name: string
|
||||
@ -8,11 +10,20 @@ export type User = {
|
||||
avatarUrl: string | null
|
||||
createdAt: Date
|
||||
generations: number
|
||||
sandbox: Sandbox[]
|
||||
usersToSandboxes: UsersToSandboxes[]
|
||||
bio: string | null
|
||||
personalWebsite: string | null
|
||||
links: UserLink[]
|
||||
tier: "FREE" | "PRO" | "ENTERPRISE"
|
||||
tierExpiresAt: Date
|
||||
lastResetDate?: number
|
||||
lastResetDate: number
|
||||
sandbox: Sandbox[]
|
||||
usersToSandboxes: UsersToSandboxes[]
|
||||
}
|
||||
|
||||
export type KnownPlatform = (typeof KNOWN_PLATFORMS)[number]
|
||||
export type UserLink = {
|
||||
url: string
|
||||
platform: KnownPlatform
|
||||
}
|
||||
|
||||
export type Sandbox = {
|
||||
|
@ -2,7 +2,7 @@ import { type ClassValue, clsx } from "clsx"
|
||||
// import { toast } from "sonner"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import fileExtToLang from "./file-extension-to-language.json"
|
||||
import { TFile, TFolder } from "./types"
|
||||
import { KnownPlatform, TFile, TFolder, UserLink } from "./types"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
@ -98,3 +98,57 @@ export function sortFileExplorer(
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
export function parseSocialLink(url: string): UserLink {
|
||||
try {
|
||||
// Handle empty or invalid URLs
|
||||
if (!url) return { url: "", platform: "generic" }
|
||||
|
||||
// Remove protocol and www prefix for consistent parsing
|
||||
const cleanUrl = url
|
||||
.toLowerCase()
|
||||
.replace(/^https?:\/\//, "")
|
||||
.replace(/^www\./, "")
|
||||
.split("/")[0] // Get just the domain part
|
||||
|
||||
// Platform detection mapping
|
||||
const platformPatterns: Record<
|
||||
Exclude<KnownPlatform, "generic">,
|
||||
RegExp
|
||||
> = {
|
||||
github: /github\.com/,
|
||||
twitter: /(?:twitter\.com|x\.com|t\.co)/,
|
||||
instagram: /instagram\.com/,
|
||||
bluesky: /(?:bsky\.app|bluesky\.social)/,
|
||||
linkedin: /linkedin\.com/,
|
||||
youtube: /(?:youtube\.com|youtu\.be)/,
|
||||
twitch: /twitch\.tv/,
|
||||
discord: /discord\.(?:gg|com)/,
|
||||
mastodon: /mastodon\.(?:social|online|world)/,
|
||||
threads: /threads\.net/,
|
||||
gitlab: /gitlab\.com/,
|
||||
}
|
||||
|
||||
// Check URL against each pattern
|
||||
for (const [platform, pattern] of Object.entries(platformPatterns)) {
|
||||
if (pattern.test(cleanUrl)) {
|
||||
return {
|
||||
url,
|
||||
platform: platform as KnownPlatform,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to generic if no match found
|
||||
return {
|
||||
url,
|
||||
platform: "generic",
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing social link:", error)
|
||||
return {
|
||||
url: url || "",
|
||||
platform: "generic",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
572
frontend/package-lock.json
generated
572
frontend/package-lock.json
generated
@ -13,7 +13,7 @@
|
||||
"@clerk/nextjs": "^4.29.12",
|
||||
"@clerk/themes": "^1.7.12",
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@liveblocks/client": "^1.12.0",
|
||||
"@liveblocks/node": "^1.12.0",
|
||||
"@liveblocks/react": "^1.12.0",
|
||||
@ -27,14 +27,15 @@
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-hover-card": "^1.1.2",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-popover": "^1.1.1",
|
||||
"@radix-ui/react-progress": "^1.1.0",
|
||||
"@radix-ui/react-scroll-area": "^1.2.2",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.1.1",
|
||||
"@radix-ui/react-tooltip": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.1.6",
|
||||
"@react-three/fiber": "^8.16.6",
|
||||
"@uiw/codemirror-theme-vscode": "^4.23.5",
|
||||
"@uiw/react-codemirror": "^4.23.5",
|
||||
@ -53,11 +54,12 @@
|
||||
"lucide-react": "^0.365.0",
|
||||
"monaco-themes": "^0.4.4",
|
||||
"next": "14.1.3",
|
||||
"next-themes": "^0.3.0",
|
||||
"next-themes": "^0.4.4",
|
||||
"openai": "^4.73.1",
|
||||
"posthog-js": "^1.147.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.51.3",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-resizable-panels": "^2.0.16",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
@ -72,7 +74,7 @@
|
||||
"y-monaco": "^0.1.5",
|
||||
"y-protocols": "^1.0.6",
|
||||
"yjs": "^13.6.15",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/estree": "^1.0.6",
|
||||
@ -908,9 +910,9 @@
|
||||
"integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
|
||||
},
|
||||
"node_modules/@hookform/resolvers": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.4.tgz",
|
||||
"integrity": "sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==",
|
||||
"version": "3.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.1.tgz",
|
||||
"integrity": "sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==",
|
||||
"peerDependencies": {
|
||||
"react-hook-form": "^7.0.0"
|
||||
}
|
||||
@ -1364,6 +1366,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
|
||||
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-arrow": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz",
|
||||
@ -1533,6 +1553,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
|
||||
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz",
|
||||
@ -1631,6 +1669,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
|
||||
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz",
|
||||
@ -2115,18 +2171,39 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-label": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.2.tgz",
|
||||
"integrity": "sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.1.tgz",
|
||||
"integrity": "sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-primitive": "1.0.3"
|
||||
"@radix-ui/react-primitive": "2.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
|
||||
"integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@ -2177,6 +2254,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
|
||||
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.1.tgz",
|
||||
@ -2692,6 +2787,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
|
||||
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-progress": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.0.tgz",
|
||||
@ -2817,6 +2930,161 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.2.tgz",
|
||||
"integrity": "sha512-EFI1N/S3YxZEW/lJ/H1jY3njlvTd8tBmgKEn4GHi51+aMm94i6NmAJstsm5cu3yJwYqYc93gpCPm21FeAbFk6g==",
|
||||
"dependencies": {
|
||||
"@radix-ui/number": "1.1.0",
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/number": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz",
|
||||
"integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ=="
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
|
||||
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
|
||||
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
|
||||
"integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
|
||||
"integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
|
||||
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
|
||||
"integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
|
||||
"integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
|
||||
"integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-select": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.0.0.tgz",
|
||||
@ -2860,7 +3128,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
|
||||
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
|
||||
@ -2878,6 +3146,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
||||
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
|
||||
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.0.3.tgz",
|
||||
@ -3191,23 +3490,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.3.tgz",
|
||||
"integrity": "sha512-Z4w1FIS0BqVFI2c1jZvb/uDVJijJjJ2ZMuPV81oVgTZ7g3BZxobplnMVvXtFWgtozdvYJ+MFWtwkM5S2HnAong==",
|
||||
"license": "MIT",
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.6.tgz",
|
||||
"integrity": "sha512-TLB5D8QLExS1uDn7+wH/bjEmRurNMTzNrtq7IjaS4kjion9NtzsTGkvR5+i7yc9q01Pi2KMM2cN3f8UG4IvvXA==",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.3",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-popper": "1.2.0",
|
||||
"@radix-ui/react-portal": "1.1.2",
|
||||
"@radix-ui/react-presence": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-slot": "1.1.0",
|
||||
"@radix-ui/react-popper": "1.2.1",
|
||||
"@radix-ui/react-portal": "1.1.3",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-slot": "1.1.1",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"@radix-ui/react-visually-hidden": "1.1.0"
|
||||
"@radix-ui/react-visually-hidden": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@ -3225,18 +3523,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
|
||||
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==",
|
||||
"license": "MIT"
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
|
||||
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
|
||||
"integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==",
|
||||
"license": "MIT",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz",
|
||||
"integrity": "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.0"
|
||||
"@radix-ui/react-primitive": "2.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@ -3254,10 +3550,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
|
||||
"integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==",
|
||||
"license": "MIT",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
|
||||
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
@ -3284,14 +3579,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz",
|
||||
"integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==",
|
||||
"license": "MIT",
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz",
|
||||
"integrity": "sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-escape-keydown": "1.1.0"
|
||||
},
|
||||
@ -3329,16 +3623,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz",
|
||||
"integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==",
|
||||
"license": "MIT",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz",
|
||||
"integrity": "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.0.0",
|
||||
"@radix-ui/react-arrow": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-context": "1.1.0",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-arrow": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||
"@radix-ui/react-use-rect": "1.1.0",
|
||||
@ -3360,28 +3653,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz",
|
||||
"integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz",
|
||||
"integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==",
|
||||
"license": "MIT",
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz",
|
||||
"integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@ -3400,12 +3677,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz",
|
||||
"integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==",
|
||||
"license": "MIT",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
|
||||
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@ -3424,12 +3700,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz",
|
||||
"integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==",
|
||||
"license": "MIT",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
|
||||
"integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.1.0"
|
||||
"@radix-ui/react-slot": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@ -3446,24 +3721,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
|
||||
@ -3501,7 +3758,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
|
||||
"integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0"
|
||||
},
|
||||
@ -3534,7 +3790,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
|
||||
"integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/rect": "1.1.0"
|
||||
},
|
||||
@ -3552,7 +3807,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz",
|
||||
"integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
@ -3567,12 +3821,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-visually-hidden": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz",
|
||||
"integrity": "sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==",
|
||||
"license": "MIT",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz",
|
||||
"integrity": "sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.0"
|
||||
"@radix-ui/react-primitive": "2.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@ -3592,8 +3845,7 @@
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/rect": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
|
||||
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
|
||||
"license": "MIT"
|
||||
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.0.1",
|
||||
@ -7324,12 +7576,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/next-themes": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz",
|
||||
"integrity": "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==",
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.4.tgz",
|
||||
"integrity": "sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17 || ^18",
|
||||
"react-dom": "^16.8 || ^17 || ^18"
|
||||
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/next/node_modules/postcss": {
|
||||
@ -7492,6 +7745,65 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "4.73.1",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.73.1.tgz",
|
||||
"integrity": "sha512-nWImDJBcUsqrhy7yJScXB4+iqjzbUEgzfA3un/6UnHFdwWhjX24oztj69Ped/njABfOdLcO/F7CeWTI5dt8Xmg==",
|
||||
"license": "Apache-2.0",
|
||||
"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"
|
||||
},
|
||||
"bin": {
|
||||
"openai": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/openai/node_modules/@types/node": {
|
||||
"version": "18.19.67",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz",
|
||||
"integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/openai/node_modules/@types/node-fetch": {
|
||||
"version": "2.6.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz",
|
||||
"integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/openai/node_modules/form-data": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
|
||||
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/ora": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/ora/-/ora-6.3.1.tgz",
|
||||
@ -7968,18 +8280,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.51.3",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.3.tgz",
|
||||
"integrity": "sha512-cvJ/wbHdhYx8aviSWh28w9ImjmVsb5Y05n1+FW786vEZQJV5STNM0pW6ujS+oiBecb0ARBxJFyAnXj9+GHXACQ==",
|
||||
"version": "7.54.2",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz",
|
||||
"integrity": "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==",
|
||||
"engines": {
|
||||
"node": ">=12.22.0"
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/react-hook-form"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17 || ^18"
|
||||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-markdown": {
|
||||
@ -9664,9 +9976,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.23.8",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
||||
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||
"version": "3.24.1",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
|
||||
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
"@clerk/nextjs": "^4.29.12",
|
||||
"@clerk/themes": "^1.7.12",
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@liveblocks/client": "^1.12.0",
|
||||
"@liveblocks/node": "^1.12.0",
|
||||
"@liveblocks/react": "^1.12.0",
|
||||
@ -28,14 +28,15 @@
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-hover-card": "^1.1.2",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-popover": "^1.1.1",
|
||||
"@radix-ui/react-progress": "^1.1.0",
|
||||
"@radix-ui/react-scroll-area": "^1.2.2",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.1.1",
|
||||
"@radix-ui/react-tooltip": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.1.6",
|
||||
"@react-three/fiber": "^8.16.6",
|
||||
"@uiw/codemirror-theme-vscode": "^4.23.5",
|
||||
"@uiw/react-codemirror": "^4.23.5",
|
||||
@ -54,11 +55,12 @@
|
||||
"lucide-react": "^0.365.0",
|
||||
"monaco-themes": "^0.4.4",
|
||||
"next": "14.1.3",
|
||||
"next-themes": "^0.3.0",
|
||||
"next-themes": "^0.4.4",
|
||||
"openai": "^4.73.1",
|
||||
"posthog-js": "^1.147.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.51.3",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-resizable-panels": "^2.0.16",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
@ -73,7 +75,7 @@
|
||||
"y-monaco": "^0.1.5",
|
||||
"y-protocols": "^1.0.6",
|
||||
"yjs": "^13.6.15",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/estree": "^1.0.6",
|
||||
|
Loading…
x
Reference in New Issue
Block a user