Merge branch 'main' of https://github.com/Code-Victor/sandbox
This commit is contained in:
commit
2195b27c7e
@ -1,4 +0,0 @@
|
|||||||
# frontend/**
|
|
||||||
backend/ai/**
|
|
||||||
backend/database/**
|
|
||||||
backend/storage/**
|
|
252
README.md
252
README.md
@ -1,103 +1,213 @@
|
|||||||
# Sandbox 📦🪄
|
# GitWit Sandbox 📦🪄
|
||||||
|
|
||||||
<img width="1799" alt="Screenshot 2024-05-31 at 8 33 56 AM" src="https://github.com/ishaan1013/sandbox/assets/69771365/3f73d7c0-f82a-4997-b01e-eaa043e95113">
|

|
||||||
|
|
||||||
Sandbox is an open-source cloud-based code editing environment with custom AI code autocompletion and real-time collaboration.
|
Sandbox is an open-source cloud-based code editing environment with custom AI code generation, live preview, real-time collaboration and AI chat.
|
||||||
|
|
||||||
Check out the [Twitter thread](https://x.com/ishaandey_/status/1796338262002573526) with the demo video!
|
For the latest updates, join our Discord server: [discord.gitwit.dev](https://discord.gitwit.dev/).
|
||||||
|
|
||||||
Check out this [guide](https://dev.to/jamesmurdza/how-to-setup-ishaan1013sandbox-locally-503p) made by [@jamesmurdza](https://x.com/jamesmurdza) on setting it up locally!
|
|
||||||
|
|
||||||
## Running Locally
|
## Running Locally
|
||||||
|
|
||||||
### Frontend
|
Notes:
|
||||||
|
|
||||||
Install dependencies
|
- Double check that whatever you change "SUPERDUPERSECRET" to, it's the same in all config files.
|
||||||
|
- Right now we are loading project templates from a custom Cloudflare bucket which isn't covered in this guide, but that be updated/fixed very soon.
|
||||||
|
|
||||||
|
### 0. Requirements
|
||||||
|
|
||||||
|
The application uses NodeJS for the backend, NextJS for the frontend and Cloudflare workers for additional backend tasks.
|
||||||
|
|
||||||
|
Needed accounts to set up:
|
||||||
|
|
||||||
|
- [Clerk](https://clerk.com/): Used for user authentication.
|
||||||
|
- [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).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### 1. Initial setup
|
||||||
|
|
||||||
|
No surprise in the first step:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
git clone https://github.com/jamesmurdza/sandbox
|
||||||
npm install
|
cd sandbox
|
||||||
```
|
```
|
||||||
|
|
||||||
Add the required environment variables in `.env` (example file provided in `.env.example`). You will need to make an account on [Clerk](https://clerk.com/) and [Liveblocks](https://liveblocks.io/) to get API keys.
|
Run `npm install` in:
|
||||||
|
|
||||||
Then, run in development mode
|
```
|
||||||
|
/frontend
|
||||||
```bash
|
/backend/database
|
||||||
npm run dev
|
/backend/storage
|
||||||
|
/backend/server
|
||||||
|
/backend/ai
|
||||||
```
|
```
|
||||||
|
|
||||||
### Backend
|
### 2. Adding Clerk
|
||||||
|
|
||||||
The backend consists of a primary Express and Socket.io server, and 3 Cloudflare Workers microservices for the D1 database, R2 storage, and Workers AI. The D1 database also contains a [service binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) to the R2 storage worker. Each open sandbox instantiates a secure Linux sandboxes on E2B, which is used for the terminal and live preview.
|
Setup the Clerk account.
|
||||||
|
Get the API keys from Clerk.
|
||||||
|
|
||||||
You will need to make an account on [E2B](https://e2b.dev/) to get an API key.
|
Update `/frontend/.env`:
|
||||||
|
|
||||||
#### Socket.io server
|
```
|
||||||
|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY='🔑'
|
||||||
Install dependencies
|
CLERK_SECRET_KEY='🔑'
|
||||||
|
|
||||||
```bash
|
|
||||||
cd backend/server
|
|
||||||
npm install
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Add the required environment variables in `.env` (example file provided in `.env.example`)
|
### 3. Deploying the storage bucket
|
||||||
|
|
||||||
Project files will be stored in the `projects/<project-id>` directory. The middleware contains basic authorization logic for connecting to the server.
|
Go to Cloudflare.
|
||||||
|
Create and name an R2 storage bucket in the control panel.
|
||||||
|
Copy the account ID of one domain.
|
||||||
|
|
||||||
Run in development mode
|
Update `/backend/storage/src/wrangler.toml`:
|
||||||
|
|
||||||
```bash
|
```
|
||||||
npm run dev
|
account_id = '🔑'
|
||||||
|
bucket_name = '🔑'
|
||||||
|
key = 'SUPERDUPERSECRET'
|
||||||
```
|
```
|
||||||
|
|
||||||
This directory is dockerized, so feel free to deploy a container on any platform of your choice! I chose not to deploy this project for public access due to costs & safety, but deploying your own for personal use should be no problem.
|
In the `/backend/storage/src` directory:
|
||||||
|
|
||||||
#### Cloudflare Workers (Database, Storage, AI)
|
|
||||||
|
|
||||||
Directories:
|
|
||||||
|
|
||||||
- `/backend/database`: D1 database
|
|
||||||
- `/backend/storage`: R2 storage
|
|
||||||
- `/backend/ai`: Workers AI
|
|
||||||
|
|
||||||
Install dependencies
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd backend/database
|
|
||||||
npm install
|
|
||||||
|
|
||||||
cd ../storage
|
|
||||||
npm install
|
|
||||||
|
|
||||||
cd ../ai
|
|
||||||
npm install
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Read the [documentation](https://developers.cloudflare.com/workers/) to learn more about workers.
|
|
||||||
|
|
||||||
For each directory, add the required environment variables in `wrangler.toml` (example file provided in `wrangler.example.toml`). For the AI worker, you can define any value you want for the `CF_AI_KEY` -- set this in other `.env` files to authorize access.
|
|
||||||
|
|
||||||
Run in development mode
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Deploy to Cloudflare with [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx wrangler deploy
|
npx wrangler deploy
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
### 4. Deploying the database
|
||||||
|
|
||||||
|
Create a database:
|
||||||
|
|
||||||
|
```
|
||||||
|
npx wrangler d1 create sandbox-database
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the output for the next setp.
|
||||||
|
|
||||||
|
Update `/backend/database/src/wrangler.toml`:
|
||||||
|
|
||||||
|
```
|
||||||
|
database_name = '🔑'
|
||||||
|
database_id = '🔑'
|
||||||
|
KEY = 'SUPERDUPERSECRET'
|
||||||
|
STORAGE_WORKER_URL = 'https://storage.🍎.workers.dev'
|
||||||
|
```
|
||||||
|
|
||||||
|
In the `/backend/database/src` directory:
|
||||||
|
|
||||||
|
```
|
||||||
|
npx wrangler deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Applying the database schema
|
||||||
|
|
||||||
|
Delete the `/backend/database/drizzle/meta` directory.
|
||||||
|
|
||||||
|
In the `/backend/database/` directory:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run generate
|
||||||
|
npx wrangler d1 execute sandbox-database --remote --file=./drizzle/0000_🍏_🍐.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Configuring the server
|
||||||
|
|
||||||
|
Update `/backend/server/.env`:
|
||||||
|
|
||||||
|
```
|
||||||
|
DATABASE_WORKER_URL='https://database.🍎.workers.dev'
|
||||||
|
STORAGE_WORKER_URL='https://storage.🍎.workers.dev'
|
||||||
|
WORKERS_KEY='SUPERDUPERSECRET'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Adding Liveblocks
|
||||||
|
|
||||||
|
Setup the Liveblocks account.
|
||||||
|
|
||||||
|
Update `/frontend/.env`:
|
||||||
|
|
||||||
|
```
|
||||||
|
NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY='🔑'
|
||||||
|
LIVEBLOCKS_SECRET_KEY='🔑'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Adding E2B
|
||||||
|
|
||||||
|
Setup the E2B account.
|
||||||
|
|
||||||
|
Update `/backend/server/.env`:
|
||||||
|
|
||||||
|
```
|
||||||
|
E2B_API_KEY='🔑'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Adding AI code generation
|
||||||
|
|
||||||
|
In the `/backend/ai` directory:
|
||||||
|
|
||||||
|
```
|
||||||
|
npx wrangler deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `/backend/server/.env`:
|
||||||
|
|
||||||
|
```
|
||||||
|
AI_WORKER_URL='https://ai.🍎.workers.dev'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10. Configuring the frontend
|
||||||
|
|
||||||
|
Update `/frontend/.env`:
|
||||||
|
|
||||||
|
```
|
||||||
|
NEXT_PUBLIC_DATABASE_WORKER_URL='https://database.🍎.workers.dev'
|
||||||
|
NEXT_PUBLIC_STORAGE_WORKER_URL='https://storage.🍎.workers.dev'
|
||||||
|
NEXT_PUBLIC_WORKERS_KEY='SUPERDUPERSECRET'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11. Running the IDE
|
||||||
|
|
||||||
|
Run `npm run dev` simultaneously in:
|
||||||
|
|
||||||
|
```
|
||||||
|
/frontend
|
||||||
|
/backend/server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setting up Deployments
|
||||||
|
|
||||||
|
The steps above do not include steps to setup [Dokku](https://github.com/dokku/dokku), which is required for deployments.
|
||||||
|
|
||||||
|
**Note:** This is completely optional to set up if you just want to run GitWit Sandbox.
|
||||||
|
|
||||||
|
Setting up deployments first requires a separate domain (such as gitwit.app, which we use).
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
DOKKU_HOST=
|
||||||
|
DOKKU_USERNAME=
|
||||||
|
DOKKU_KEY=
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating Custom Templates
|
||||||
|
|
||||||
|
We're working on a process whereby anyone can contribute a custom template that others can use in the Sandbox environment. The process includes:
|
||||||
|
|
||||||
|
- Creating a [custom E2B Sandbox](https://e2b.dev/docs/sandbox-template) including the template files and dependencies
|
||||||
|
- Creating a file to specify the run command (e.g. "npm run dev")
|
||||||
|
- Testing the template with Dokku for deployment
|
||||||
|
|
||||||
|
Please reach out to us [on Discord](https://discord.gitwit.dev/) if you're interested in contributing.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Thanks for your interest in contributing! Review this section before submitting your first pull request. If you need any help, feel free to reach out to [@ishaandey\_](https://x.com/ishaandey_).
|
Thanks for your interest in contributing! Review this section before submitting your first pull request. If you need any help, feel free contact us [on Discord](https://discord.gitwit.dev/).
|
||||||
|
|
||||||
Please prioritize existing issues, but feel free to contribute new issues if you have ideas for a feature or bug that you think would be useful.
|
|
||||||
|
|
||||||
### Structure
|
### Structure
|
||||||
|
|
||||||
@ -117,7 +227,7 @@ backend/
|
|||||||
```
|
```
|
||||||
|
|
||||||
| Path | Description |
|
| Path | Description |
|
||||||
| ------------------ | -------------------------------------------------------------------------- |
|
| ------------------ | ------------------------------------------------------------ |
|
||||||
| `frontend` | The Next.js application for the frontend. |
|
| `frontend` | The Next.js application for the frontend. |
|
||||||
| `backend/server` | The Express websocket server. |
|
| `backend/server` | The Express websocket server. |
|
||||||
| `backend/database` | API for interfacing with the D1 database (SQLite). |
|
| `backend/database` | API for interfacing with the D1 database (SQLite). |
|
||||||
@ -151,11 +261,15 @@ It should be in the form `category(scope or module): message` in your commit mes
|
|||||||
|
|
||||||
- `feat / feature`: all changes that introduce completely new code or new
|
- `feat / feature`: all changes that introduce completely new code or new
|
||||||
features
|
features
|
||||||
|
|
||||||
- `fix`: changes that fix a bug (ideally you will additionally reference an
|
- `fix`: changes that fix a bug (ideally you will additionally reference an
|
||||||
issue if present)
|
issue if present)
|
||||||
|
|
||||||
- `refactor`: any code related change that is not a fix nor a feature
|
- `refactor`: any code related change that is not a fix nor a feature
|
||||||
|
|
||||||
- `docs`: changing existing or creating new documentation (i.e. README, docs for
|
- `docs`: changing existing or creating new documentation (i.e. README, docs for
|
||||||
usage of a lib or cli usage)
|
usage of a lib or cli usage)
|
||||||
|
|
||||||
- `chore`: all changes to the repository that do not fit into any of the above
|
- `chore`: all changes to the repository that do not fit into any of the above
|
||||||
categories
|
categories
|
||||||
|
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"tabWidth": 2,
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": false
|
|
||||||
}
|
|
@ -1,8 +1,8 @@
|
|||||||
import { Anthropic } from "@anthropic-ai/sdk";
|
import { Anthropic } from "@anthropic-ai/sdk"
|
||||||
import { MessageParam } from "@anthropic-ai/sdk/src/resources/messages.js";
|
import { MessageParam } from "@anthropic-ai/sdk/src/resources/messages.js"
|
||||||
|
|
||||||
export interface Env {
|
export interface Env {
|
||||||
ANTHROPIC_API_KEY: string;
|
ANTHROPIC_API_KEY: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -15,41 +15,45 @@ export default {
|
|||||||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
||||||
"Access-Control-Allow-Headers": "Content-Type",
|
"Access-Control-Allow-Headers": "Content-Type",
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.method !== "GET" && request.method !== "POST") {
|
if (request.method !== "GET" && request.method !== "POST") {
|
||||||
return new Response("Method Not Allowed", { status: 405 });
|
return new Response("Method Not Allowed", { status: 405 })
|
||||||
}
|
}
|
||||||
|
|
||||||
let body;
|
let body
|
||||||
let isEditCodeWidget = false;
|
let isEditCodeWidget = false
|
||||||
if (request.method === "POST") {
|
if (request.method === "POST") {
|
||||||
body = await request.json() as { messages: unknown; context: unknown; activeFileContent: string };
|
body = (await request.json()) as {
|
||||||
|
messages: unknown
|
||||||
|
context: unknown
|
||||||
|
activeFileContent: string
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url)
|
||||||
const fileName = url.searchParams.get("fileName") || "";
|
const fileName = url.searchParams.get("fileName") || ""
|
||||||
const code = url.searchParams.get("code") || "";
|
const code = url.searchParams.get("code") || ""
|
||||||
const line = url.searchParams.get("line") || "";
|
const line = url.searchParams.get("line") || ""
|
||||||
const instructions = url.searchParams.get("instructions") || "";
|
const instructions = url.searchParams.get("instructions") || ""
|
||||||
|
|
||||||
body = {
|
body = {
|
||||||
messages: [{ role: "human", content: instructions }],
|
messages: [{ role: "human", content: instructions }],
|
||||||
context: `File: ${fileName}\nLine: ${line}\nCode:\n${code}`,
|
context: `File: ${fileName}\nLine: ${line}\nCode:\n${code}`,
|
||||||
activeFileContent: code,
|
activeFileContent: code,
|
||||||
};
|
}
|
||||||
isEditCodeWidget = true;
|
isEditCodeWidget = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = body.messages;
|
const messages = body.messages
|
||||||
const context = body.context;
|
const context = body.context
|
||||||
const activeFileContent = body.activeFileContent;
|
const activeFileContent = body.activeFileContent
|
||||||
|
|
||||||
if (!Array.isArray(messages) || messages.length === 0) {
|
if (!Array.isArray(messages) || messages.length === 0) {
|
||||||
return new Response("Invalid or empty messages", { status: 400 });
|
return new Response("Invalid or empty messages", { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
let systemMessage;
|
let systemMessage
|
||||||
if (isEditCodeWidget) {
|
if (isEditCodeWidget) {
|
||||||
systemMessage = `You are an AI code editor. Your task is to modify the given code based on the user's instructions. Only output the modified code, without any explanations or markdown formatting. The code should be a direct replacement for the existing code.
|
systemMessage = `You are an AI code editor. Your task is to modify the given code based on the user's instructions. Only output the modified code, without any explanations or markdown formatting. The code should be a direct replacement for the existing code.
|
||||||
|
|
||||||
@ -61,7 +65,7 @@ ${activeFileContent}
|
|||||||
|
|
||||||
Instructions: ${messages[0].content}
|
Instructions: ${messages[0].content}
|
||||||
|
|
||||||
Respond only with the modified code that can directly replace the existing code.`;
|
Respond only with the modified code that can directly replace the existing code.`
|
||||||
} else {
|
} else {
|
||||||
systemMessage = `You are an intelligent programming assistant. 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. 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:
|
||||||
|
|
||||||
@ -71,17 +75,17 @@ print("Hello, World!")
|
|||||||
|
|
||||||
Provide a clear and concise explanation along with any code snippets. Keep your response brief and to the point.
|
Provide a clear and concise explanation along with any code snippets. Keep your response brief and to the point.
|
||||||
|
|
||||||
${context ? `Context:\n${context}\n` : ''}
|
${context ? `Context:\n${context}\n` : ""}
|
||||||
${activeFileContent ? `Active File Content:\n${activeFileContent}\n` : ''}`;
|
${activeFileContent ? `Active File Content:\n${activeFileContent}\n` : ""}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const anthropicMessages = messages.map(msg => ({
|
const anthropicMessages = messages.map((msg) => ({
|
||||||
role: msg.role === 'human' ? 'user' : 'assistant',
|
role: msg.role === "human" ? "user" : "assistant",
|
||||||
content: msg.content
|
content: msg.content,
|
||||||
})) as MessageParam[];
|
})) as MessageParam[]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY });
|
const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY })
|
||||||
|
|
||||||
const stream = await anthropic.messages.create({
|
const stream = await anthropic.messages.create({
|
||||||
model: "claude-3-5-sonnet-20240620",
|
model: "claude-3-5-sonnet-20240620",
|
||||||
@ -89,33 +93,36 @@ ${activeFileContent ? `Active File Content:\n${activeFileContent}\n` : ''}`;
|
|||||||
system: systemMessage,
|
system: systemMessage,
|
||||||
messages: anthropicMessages,
|
messages: anthropicMessages,
|
||||||
stream: true,
|
stream: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder()
|
||||||
|
|
||||||
const streamResponse = new ReadableStream({
|
const streamResponse = new ReadableStream({
|
||||||
async start(controller) {
|
async start(controller) {
|
||||||
for await (const chunk of stream) {
|
for await (const chunk of stream) {
|
||||||
if (chunk.type === 'content_block_delta' && chunk.delta.type === 'text_delta') {
|
if (
|
||||||
const bytes = encoder.encode(chunk.delta.text);
|
chunk.type === "content_block_delta" &&
|
||||||
controller.enqueue(bytes);
|
chunk.delta.type === "text_delta"
|
||||||
|
) {
|
||||||
|
const bytes = encoder.encode(chunk.delta.text)
|
||||||
|
controller.enqueue(bytes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
controller.close();
|
controller.close()
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
return new Response(streamResponse, {
|
return new Response(streamResponse, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "text/plain; charset=utf-8",
|
"Content-Type": "text/plain; charset=utf-8",
|
||||||
"Access-Control-Allow-Origin": "*",
|
"Access-Control-Allow-Origin": "*",
|
||||||
"Cache-Control": "no-cache",
|
"Cache-Control": "no-cache",
|
||||||
"Connection": "keep-alive",
|
Connection: "keep-alive",
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error:", error);
|
console.error("Error:", error)
|
||||||
return new Response("Internal Server Error", { status: 500 });
|
return new Response("Internal Server Error", { status: 500 })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
@ -1,25 +1,30 @@
|
|||||||
// test/index.spec.ts
|
// test/index.spec.ts
|
||||||
import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test';
|
import {
|
||||||
import { describe, it, expect } from 'vitest';
|
createExecutionContext,
|
||||||
import worker from '../src/index';
|
env,
|
||||||
|
SELF,
|
||||||
|
waitOnExecutionContext,
|
||||||
|
} from "cloudflare:test"
|
||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
import worker from "../src/index"
|
||||||
|
|
||||||
// For now, you'll need to do something like this to get a correctly-typed
|
// For now, you'll need to do something like this to get a correctly-typed
|
||||||
// `Request` to pass to `worker.fetch()`.
|
// `Request` to pass to `worker.fetch()`.
|
||||||
const IncomingRequest = Request<unknown, IncomingRequestCfProperties>;
|
const IncomingRequest = Request<unknown, IncomingRequestCfProperties>
|
||||||
|
|
||||||
describe('Hello World worker', () => {
|
describe("Hello World worker", () => {
|
||||||
it('responds with Hello World! (unit style)', async () => {
|
it("responds with Hello World! (unit style)", async () => {
|
||||||
const request = new IncomingRequest('http://example.com');
|
const request = new IncomingRequest("http://example.com")
|
||||||
// Create an empty context to pass to `worker.fetch()`.
|
// Create an empty context to pass to `worker.fetch()`.
|
||||||
const ctx = createExecutionContext();
|
const ctx = createExecutionContext()
|
||||||
const response = await worker.fetch(request, env, ctx);
|
const response = await worker.fetch(request, env, ctx)
|
||||||
// Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
|
// Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
|
||||||
await waitOnExecutionContext(ctx);
|
await waitOnExecutionContext(ctx)
|
||||||
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
|
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`)
|
||||||
});
|
})
|
||||||
|
|
||||||
it('responds with Hello World! (integration style)', async () => {
|
it("responds with Hello World! (integration style)", async () => {
|
||||||
const response = await SELF.fetch('https://example.com');
|
const response = await SELF.fetch("https://example.com")
|
||||||
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
|
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`)
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
@ -12,7 +12,9 @@
|
|||||||
|
|
||||||
/* Language and Environment */
|
/* Language and Environment */
|
||||||
"target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
"target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||||
"lib": ["es2021"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
|
"lib": [
|
||||||
|
"es2021"
|
||||||
|
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
|
||||||
"jsx": "react" /* Specify what JSX code is generated. */,
|
"jsx": "react" /* Specify what JSX code is generated. */,
|
||||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
|
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"
|
||||||
|
|
||||||
export default defineWorkersConfig({
|
export default defineWorkersConfig({
|
||||||
test: {
|
test: {
|
||||||
@ -8,4 +8,4 @@ export default defineWorkersConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
3
backend/ai/worker-configuration.d.ts
vendored
3
backend/ai/worker-configuration.d.ts
vendored
@ -1,4 +1,3 @@
|
|||||||
// Generated by Wrangler
|
// Generated by Wrangler
|
||||||
// After adding bindings to `wrangler.toml`, regenerate this interface via `npm run cf-typegen`
|
// After adding bindings to `wrangler.toml`, regenerate this interface via `npm run cf-typegen`
|
||||||
interface Env {
|
interface Env {}
|
||||||
}
|
|
||||||
|
@ -5,3 +5,6 @@ compatibility_flags = ["nodejs_compat"]
|
|||||||
|
|
||||||
[ai]
|
[ai]
|
||||||
binding = "AI"
|
binding = "AI"
|
||||||
|
|
||||||
|
[vars]
|
||||||
|
ANTHROPIC_API_KEY = ""
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"tabWidth": 2,
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": false
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
import type { Config } from "drizzle-kit";
|
import type { Config } from "drizzle-kit"
|
||||||
|
|
||||||
export default process.env.LOCAL_DB_PATH
|
export default process.env.LOCAL_DB_PATH
|
||||||
? ({
|
? ({
|
||||||
@ -16,4 +16,4 @@ export default process.env.LOCAL_DB_PATH
|
|||||||
wranglerConfigPath: "wrangler.toml",
|
wranglerConfigPath: "wrangler.toml",
|
||||||
dbName: "d1-sandbox",
|
dbName: "d1-sandbox",
|
||||||
},
|
},
|
||||||
} satisfies Config);
|
} satisfies Config)
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
// import type { DrizzleD1Database } from "drizzle-orm/d1";
|
// import type { DrizzleD1Database } from "drizzle-orm/d1";
|
||||||
import { drizzle } from "drizzle-orm/d1"
|
import { drizzle } from "drizzle-orm/d1"
|
||||||
import { json } from "itty-router-extras"
|
import { json } from "itty-router-extras"
|
||||||
import { ZodError, z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { user, sandbox, usersToSandboxes } from "./schema"
|
|
||||||
import * as schema from "./schema"
|
|
||||||
import { and, eq, sql } from "drizzle-orm"
|
import { and, eq, sql } from "drizzle-orm"
|
||||||
|
import * as schema from "./schema"
|
||||||
|
import { sandbox, user, usersToSandboxes } from "./schema"
|
||||||
|
|
||||||
export interface Env {
|
export interface Env {
|
||||||
DB: D1Database
|
DB: D1Database
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
import { createId } from "@paralleldrive/cuid2"
|
||||||
import { createId } from "@paralleldrive/cuid2";
|
import { relations } from "drizzle-orm"
|
||||||
import { relations, sql } from "drizzle-orm";
|
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"
|
||||||
|
|
||||||
export const user = sqliteTable("user", {
|
export const user = sqliteTable("user", {
|
||||||
id: text("id")
|
id: text("id")
|
||||||
@ -11,14 +11,14 @@ export const user = sqliteTable("user", {
|
|||||||
email: text("email").notNull(),
|
email: text("email").notNull(),
|
||||||
image: text("image"),
|
image: text("image"),
|
||||||
generations: integer("generations").default(0),
|
generations: integer("generations").default(0),
|
||||||
});
|
})
|
||||||
|
|
||||||
export type User = typeof user.$inferSelect;
|
export type User = typeof user.$inferSelect
|
||||||
|
|
||||||
export const userRelations = relations(user, ({ many }) => ({
|
export const userRelations = relations(user, ({ many }) => ({
|
||||||
sandbox: many(sandbox),
|
sandbox: many(sandbox),
|
||||||
usersToSandboxes: many(usersToSandboxes),
|
usersToSandboxes: many(usersToSandboxes),
|
||||||
}));
|
}))
|
||||||
|
|
||||||
export const sandbox = sqliteTable("sandbox", {
|
export const sandbox = sqliteTable("sandbox", {
|
||||||
id: text("id")
|
id: text("id")
|
||||||
@ -32,9 +32,9 @@ export const sandbox = sqliteTable("sandbox", {
|
|||||||
userId: text("user_id")
|
userId: text("user_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => user.id),
|
.references(() => user.id),
|
||||||
});
|
})
|
||||||
|
|
||||||
export type Sandbox = typeof sandbox.$inferSelect;
|
export type Sandbox = typeof sandbox.$inferSelect
|
||||||
|
|
||||||
export const sandboxRelations = relations(sandbox, ({ one, many }) => ({
|
export const sandboxRelations = relations(sandbox, ({ one, many }) => ({
|
||||||
author: one(user, {
|
author: one(user, {
|
||||||
@ -42,7 +42,7 @@ export const sandboxRelations = relations(sandbox, ({ one, many }) => ({
|
|||||||
references: [user.id],
|
references: [user.id],
|
||||||
}),
|
}),
|
||||||
usersToSandboxes: many(usersToSandboxes),
|
usersToSandboxes: many(usersToSandboxes),
|
||||||
}));
|
}))
|
||||||
|
|
||||||
export const usersToSandboxes = sqliteTable("users_to_sandboxes", {
|
export const usersToSandboxes = sqliteTable("users_to_sandboxes", {
|
||||||
userId: text("userId")
|
userId: text("userId")
|
||||||
@ -52,9 +52,11 @@ export const usersToSandboxes = sqliteTable("users_to_sandboxes", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
.references(() => sandbox.id),
|
.references(() => sandbox.id),
|
||||||
sharedOn: integer("sharedOn", { mode: "timestamp_ms" }),
|
sharedOn: integer("sharedOn", { mode: "timestamp_ms" }),
|
||||||
});
|
})
|
||||||
|
|
||||||
export const usersToSandboxesRelations = relations(usersToSandboxes, ({ one }) => ({
|
export const usersToSandboxesRelations = relations(
|
||||||
|
usersToSandboxes,
|
||||||
|
({ one }) => ({
|
||||||
group: one(sandbox, {
|
group: one(sandbox, {
|
||||||
fields: [usersToSandboxes.sandboxId],
|
fields: [usersToSandboxes.sandboxId],
|
||||||
references: [sandbox.id],
|
references: [sandbox.id],
|
||||||
@ -63,4 +65,5 @@ export const usersToSandboxesRelations = relations(usersToSandboxes, ({ one }) =
|
|||||||
fields: [usersToSandboxes.userId],
|
fields: [usersToSandboxes.userId],
|
||||||
references: [user.id],
|
references: [user.id],
|
||||||
}),
|
}),
|
||||||
}));
|
})
|
||||||
|
)
|
||||||
|
@ -1,25 +1,30 @@
|
|||||||
// test/index.spec.ts
|
// test/index.spec.ts
|
||||||
import { env, createExecutionContext, waitOnExecutionContext, SELF } from "cloudflare:test";
|
import {
|
||||||
import { describe, it, expect } from "vitest";
|
createExecutionContext,
|
||||||
import worker from "../src/index";
|
env,
|
||||||
|
SELF,
|
||||||
|
waitOnExecutionContext,
|
||||||
|
} from "cloudflare:test"
|
||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
import worker from "../src/index"
|
||||||
|
|
||||||
// For now, you'll need to do something like this to get a correctly-typed
|
// For now, you'll need to do something like this to get a correctly-typed
|
||||||
// `Request` to pass to `worker.fetch()`.
|
// `Request` to pass to `worker.fetch()`.
|
||||||
const IncomingRequest = Request<unknown, IncomingRequestCfProperties>;
|
const IncomingRequest = Request<unknown, IncomingRequestCfProperties>
|
||||||
|
|
||||||
describe("Hello World worker", () => {
|
describe("Hello World worker", () => {
|
||||||
it("responds with Hello World! (unit style)", async () => {
|
it("responds with Hello World! (unit style)", async () => {
|
||||||
const request = new IncomingRequest("http://example.com");
|
const request = new IncomingRequest("http://example.com")
|
||||||
// Create an empty context to pass to `worker.fetch()`.
|
// Create an empty context to pass to `worker.fetch()`.
|
||||||
const ctx = createExecutionContext();
|
const ctx = createExecutionContext()
|
||||||
const response = await worker.fetch(request, env, ctx);
|
const response = await worker.fetch(request, env, ctx)
|
||||||
// Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
|
// Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
|
||||||
await waitOnExecutionContext(ctx);
|
await waitOnExecutionContext(ctx)
|
||||||
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
|
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`)
|
||||||
});
|
})
|
||||||
|
|
||||||
it("responds with Hello World! (integration style)", async () => {
|
it("responds with Hello World! (integration style)", async () => {
|
||||||
const response = await SELF.fetch("https://example.com");
|
const response = await SELF.fetch("https://example.com")
|
||||||
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
|
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`)
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
@ -12,7 +12,9 @@
|
|||||||
|
|
||||||
/* Language and Environment */
|
/* Language and Environment */
|
||||||
"target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
"target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||||
"lib": ["es2021"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
|
"lib": [
|
||||||
|
"es2021"
|
||||||
|
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
|
||||||
"jsx": "react" /* Specify what JSX code is generated. */,
|
"jsx": "react" /* Specify what JSX code is generated. */,
|
||||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
|
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"
|
||||||
|
|
||||||
export default defineWorkersConfig({
|
export default defineWorkersConfig({
|
||||||
test: {
|
test: {
|
||||||
@ -8,4 +8,4 @@ export default defineWorkersConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
58
backend/server/src/ConnectionManager.ts
Normal file
58
backend/server/src/ConnectionManager.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { Socket } from "socket.io"
|
||||||
|
|
||||||
|
class Counter {
|
||||||
|
private count: number = 0
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.count++
|
||||||
|
}
|
||||||
|
|
||||||
|
decrement() {
|
||||||
|
this.count = Math.max(0, this.count - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
getValue(): number {
|
||||||
|
return this.count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Owner Connection Management
|
||||||
|
export class ConnectionManager {
|
||||||
|
// Counts how many times the owner is connected to a sandbox
|
||||||
|
private ownerConnections: Record<string, Counter> = {}
|
||||||
|
// Stores all sockets connected to a given sandbox
|
||||||
|
private sockets: Record<string, Set<Socket>> = {}
|
||||||
|
|
||||||
|
// Checks if the owner of a sandbox is connected
|
||||||
|
ownerIsConnected(sandboxId: string): boolean {
|
||||||
|
return this.ownerConnections[sandboxId]?.getValue() > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds a connection for a sandbox
|
||||||
|
addConnectionForSandbox(socket: Socket, sandboxId: string, isOwner: boolean) {
|
||||||
|
this.sockets[sandboxId] ??= new Set()
|
||||||
|
this.sockets[sandboxId].add(socket)
|
||||||
|
|
||||||
|
// If the connection is for the owner, increments the owner connection counter
|
||||||
|
if (isOwner) {
|
||||||
|
this.ownerConnections[sandboxId] ??= new Counter()
|
||||||
|
this.ownerConnections[sandboxId].increment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes a connection for a sandbox
|
||||||
|
removeConnectionForSandbox(socket: Socket, sandboxId: string, isOwner: boolean) {
|
||||||
|
this.sockets[sandboxId]?.delete(socket)
|
||||||
|
|
||||||
|
// If the connection being removed is for the owner, decrements the owner connection counter
|
||||||
|
if (isOwner) {
|
||||||
|
this.ownerConnections[sandboxId]?.decrement()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the set of sockets connected to a given sandbox
|
||||||
|
connectionsForSandbox(sandboxId: string): Set<Socket> {
|
||||||
|
return this.sockets[sandboxId] ?? new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -4,12 +4,6 @@ import RemoteFileStorage from "./RemoteFileStorage"
|
|||||||
import { MAX_BODY_SIZE } from "./ratelimit"
|
import { MAX_BODY_SIZE } from "./ratelimit"
|
||||||
import { TFile, TFileData, TFolder } from "./types"
|
import { TFile, TFileData, TFolder } from "./types"
|
||||||
|
|
||||||
// Define the structure for sandbox files
|
|
||||||
export type SandboxFiles = {
|
|
||||||
files: (TFolder | TFile)[]
|
|
||||||
fileData: TFileData[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert list of paths to the hierchical file structure used by the editor
|
// Convert list of paths to the hierchical file structure used by the editor
|
||||||
function generateFileStructure(paths: string[]): (TFolder | TFile)[] {
|
function generateFileStructure(paths: string[]): (TFolder | TFile)[] {
|
||||||
const root: TFolder = { id: "/", type: "folder", name: "/", children: [] }
|
const root: TFolder = { id: "/", type: "folder", name: "/", children: [] }
|
||||||
@ -52,20 +46,22 @@ function generateFileStructure(paths: string[]): (TFolder | TFile)[] {
|
|||||||
export class FileManager {
|
export class FileManager {
|
||||||
private sandboxId: string
|
private sandboxId: string
|
||||||
private sandbox: Sandbox
|
private sandbox: Sandbox
|
||||||
public sandboxFiles: SandboxFiles
|
public files: (TFolder | TFile)[]
|
||||||
|
public fileData: TFileData[]
|
||||||
private fileWatchers: WatchHandle[] = []
|
private fileWatchers: WatchHandle[] = []
|
||||||
private dirName = "/home/user/project"
|
private dirName = "/home/user/project"
|
||||||
private refreshFileList: (files: SandboxFiles) => void
|
private refreshFileList: ((files: (TFolder | TFile)[]) => void) | null
|
||||||
|
|
||||||
// Constructor to initialize the FileManager
|
// Constructor to initialize the FileManager
|
||||||
constructor(
|
constructor(
|
||||||
sandboxId: string,
|
sandboxId: string,
|
||||||
sandbox: Sandbox,
|
sandbox: Sandbox,
|
||||||
refreshFileList: (files: SandboxFiles) => void
|
refreshFileList: ((files: (TFolder | TFile)[]) => void) | null
|
||||||
) {
|
) {
|
||||||
this.sandboxId = sandboxId
|
this.sandboxId = sandboxId
|
||||||
this.sandbox = sandbox
|
this.sandbox = sandbox
|
||||||
this.sandboxFiles = { files: [], fileData: [] }
|
this.files = []
|
||||||
|
this.fileData = []
|
||||||
this.refreshFileList = refreshFileList
|
this.refreshFileList = refreshFileList
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,16 +106,16 @@ export class FileManager {
|
|||||||
private async updateFileData(): Promise<TFileData[]> {
|
private async updateFileData(): Promise<TFileData[]> {
|
||||||
const remotePaths = await RemoteFileStorage.getSandboxPaths(this.sandboxId)
|
const remotePaths = await RemoteFileStorage.getSandboxPaths(this.sandboxId)
|
||||||
const localPaths = this.getLocalFileIds(remotePaths)
|
const localPaths = this.getLocalFileIds(remotePaths)
|
||||||
this.sandboxFiles.fileData = await this.generateFileData(localPaths)
|
this.fileData = await this.generateFileData(localPaths)
|
||||||
return this.sandboxFiles.fileData
|
return this.fileData
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update file structure
|
// Update file structure
|
||||||
private async updateFileStructure(): Promise<(TFolder | TFile)[]> {
|
private async updateFileStructure(): Promise<(TFolder | TFile)[]> {
|
||||||
const remotePaths = await RemoteFileStorage.getSandboxPaths(this.sandboxId)
|
const remotePaths = await RemoteFileStorage.getSandboxPaths(this.sandboxId)
|
||||||
const localPaths = this.getLocalFileIds(remotePaths)
|
const localPaths = this.getLocalFileIds(remotePaths)
|
||||||
this.sandboxFiles.files = generateFileStructure(localPaths)
|
this.files = generateFileStructure(localPaths)
|
||||||
return this.sandboxFiles.files
|
return this.files
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the FileManager
|
// Initialize the FileManager
|
||||||
@ -130,9 +126,9 @@ export class FileManager {
|
|||||||
await this.updateFileData()
|
await this.updateFileData()
|
||||||
|
|
||||||
// Copy all files from the project to the container
|
// Copy all files from the project to the container
|
||||||
const promises = this.sandboxFiles.fileData.map(async (file) => {
|
const promises = this.fileData.map(async (file) => {
|
||||||
try {
|
try {
|
||||||
const filePath = path.join(this.dirName, file.id)
|
const filePath = path.posix.join(this.dirName, file.id)
|
||||||
const parentDirectory = path.dirname(filePath)
|
const parentDirectory = path.dirname(filePath)
|
||||||
if (!this.sandbox.files.exists(parentDirectory)) {
|
if (!this.sandbox.files.exists(parentDirectory)) {
|
||||||
await this.sandbox.files.makeDir(parentDirectory)
|
await this.sandbox.files.makeDir(parentDirectory)
|
||||||
@ -209,7 +205,7 @@ export class FileManager {
|
|||||||
// Handle file/directory creation event
|
// Handle file/directory creation event
|
||||||
if (event.type === "create") {
|
if (event.type === "create") {
|
||||||
const folder = findFolderById(
|
const folder = findFolderById(
|
||||||
this.sandboxFiles.files,
|
this.files,
|
||||||
sandboxDirectory
|
sandboxDirectory
|
||||||
) as TFolder
|
) as TFolder
|
||||||
const isDir = await this.isDirectory(containerFilePath)
|
const isDir = await this.isDirectory(containerFilePath)
|
||||||
@ -232,7 +228,7 @@ export class FileManager {
|
|||||||
folder.children.push(newItem)
|
folder.children.push(newItem)
|
||||||
} else {
|
} else {
|
||||||
// If folder doesn't exist, add the new item to the root
|
// If folder doesn't exist, add the new item to the root
|
||||||
this.sandboxFiles.files.push(newItem)
|
this.files.push(newItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDir) {
|
if (!isDir) {
|
||||||
@ -241,7 +237,7 @@ export class FileManager {
|
|||||||
)
|
)
|
||||||
const fileContents =
|
const fileContents =
|
||||||
typeof fileData === "string" ? fileData : ""
|
typeof fileData === "string" ? fileData : ""
|
||||||
this.sandboxFiles.fileData.push({
|
this.fileData.push({
|
||||||
id: sandboxFilePath,
|
id: sandboxFilePath,
|
||||||
data: fileContents,
|
data: fileContents,
|
||||||
})
|
})
|
||||||
@ -253,7 +249,7 @@ export class FileManager {
|
|||||||
// Handle file/directory removal or rename event
|
// Handle file/directory removal or rename event
|
||||||
else if (event.type === "remove" || event.type == "rename") {
|
else if (event.type === "remove" || event.type == "rename") {
|
||||||
const folder = findFolderById(
|
const folder = findFolderById(
|
||||||
this.sandboxFiles.files,
|
this.files,
|
||||||
sandboxDirectory
|
sandboxDirectory
|
||||||
) as TFolder
|
) as TFolder
|
||||||
const isDir = await this.isDirectory(containerFilePath)
|
const isDir = await this.isDirectory(containerFilePath)
|
||||||
@ -269,13 +265,13 @@ export class FileManager {
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Remove from the root if it's not inside a folder
|
// Remove from the root if it's not inside a folder
|
||||||
this.sandboxFiles.files = this.sandboxFiles.files.filter(
|
this.files = this.files.filter(
|
||||||
(file: TFolder | TFile) => !isFileMatch(file)
|
(file: TFolder | TFile) => !isFileMatch(file)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also remove any corresponding file data
|
// Also remove any corresponding file data
|
||||||
this.sandboxFiles.fileData = this.sandboxFiles.fileData.filter(
|
this.fileData = this.fileData.filter(
|
||||||
(file: TFileData) => !isFileMatch(file)
|
(file: TFileData) => !isFileMatch(file)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -285,10 +281,10 @@ export class FileManager {
|
|||||||
// Handle file write event
|
// Handle file write event
|
||||||
else if (event.type === "write") {
|
else if (event.type === "write") {
|
||||||
const folder = findFolderById(
|
const folder = findFolderById(
|
||||||
this.sandboxFiles.files,
|
this.files,
|
||||||
sandboxDirectory
|
sandboxDirectory
|
||||||
) as TFolder
|
) as TFolder
|
||||||
const fileToWrite = this.sandboxFiles.fileData.find(
|
const fileToWrite = this.fileData.find(
|
||||||
(file) => file.id === sandboxFilePath
|
(file) => file.id === sandboxFilePath
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -308,7 +304,7 @@ export class FileManager {
|
|||||||
)
|
)
|
||||||
const fileContents =
|
const fileContents =
|
||||||
typeof fileData === "string" ? fileData : ""
|
typeof fileData === "string" ? fileData : ""
|
||||||
this.sandboxFiles.fileData.push({
|
this.fileData.push({
|
||||||
id: sandboxFilePath,
|
id: sandboxFilePath,
|
||||||
data: fileContents,
|
data: fileContents,
|
||||||
})
|
})
|
||||||
@ -318,7 +314,9 @@ export class FileManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tell the client to reload the file list
|
// Tell the client to reload the file list
|
||||||
this.refreshFileList(this.sandboxFiles)
|
if (event.type !== "chmod") {
|
||||||
|
this.refreshFileList?.(this.files)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
`Error handling ${event.type} event for ${event.name}:`,
|
`Error handling ${event.type} event for ${event.name}:`,
|
||||||
@ -350,7 +348,7 @@ export class FileManager {
|
|||||||
|
|
||||||
// Get file content
|
// Get file content
|
||||||
async getFile(fileId: string): Promise<string | undefined> {
|
async getFile(fileId: string): Promise<string | undefined> {
|
||||||
const file = this.sandboxFiles.fileData.find((f) => f.id === fileId)
|
const file = this.fileData.find((f) => f.id === fileId)
|
||||||
return file?.data
|
return file?.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -368,7 +366,7 @@ export class FileManager {
|
|||||||
throw new Error("File size too large. Please reduce the file size.")
|
throw new Error("File size too large. Please reduce the file size.")
|
||||||
}
|
}
|
||||||
await RemoteFileStorage.saveFile(this.getRemoteFileId(fileId), body)
|
await RemoteFileStorage.saveFile(this.getRemoteFileId(fileId), body)
|
||||||
const file = this.sandboxFiles.fileData.find((f) => f.id === fileId)
|
const file = this.fileData.find((f) => f.id === fileId)
|
||||||
if (!file) return
|
if (!file) return
|
||||||
file.data = body
|
file.data = body
|
||||||
|
|
||||||
@ -381,9 +379,9 @@ export class FileManager {
|
|||||||
fileId: string,
|
fileId: string,
|
||||||
folderId: string
|
folderId: string
|
||||||
): Promise<(TFolder | TFile)[]> {
|
): Promise<(TFolder | TFile)[]> {
|
||||||
const fileData = this.sandboxFiles.fileData.find((f) => f.id === fileId)
|
const fileData = this.fileData.find((f) => f.id === fileId)
|
||||||
const file = this.sandboxFiles.files.find((f) => f.id === fileId)
|
const file = this.files.find((f) => f.id === fileId)
|
||||||
if (!fileData || !file) return this.sandboxFiles.files
|
if (!fileData || !file) return this.files
|
||||||
|
|
||||||
const parts = fileId.split("/")
|
const parts = fileId.split("/")
|
||||||
const newFileId = folderId + "/" + parts.pop()
|
const newFileId = folderId + "/" + parts.pop()
|
||||||
@ -427,13 +425,13 @@ export class FileManager {
|
|||||||
await this.sandbox.files.write(path.posix.join(this.dirName, id), "")
|
await this.sandbox.files.write(path.posix.join(this.dirName, id), "")
|
||||||
await this.fixPermissions()
|
await this.fixPermissions()
|
||||||
|
|
||||||
this.sandboxFiles.files.push({
|
this.files.push({
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
type: "file",
|
type: "file",
|
||||||
})
|
})
|
||||||
|
|
||||||
this.sandboxFiles.fileData.push({
|
this.fileData.push({
|
||||||
id,
|
id,
|
||||||
data: "",
|
data: "",
|
||||||
})
|
})
|
||||||
@ -451,8 +449,8 @@ export class FileManager {
|
|||||||
|
|
||||||
// Rename a file
|
// Rename a file
|
||||||
async renameFile(fileId: string, newName: string): Promise<void> {
|
async renameFile(fileId: string, newName: string): Promise<void> {
|
||||||
const fileData = this.sandboxFiles.fileData.find((f) => f.id === fileId)
|
const fileData = this.fileData.find((f) => f.id === fileId)
|
||||||
const file = this.sandboxFiles.files.find((f) => f.id === fileId)
|
const file = this.files.find((f) => f.id === fileId)
|
||||||
if (!fileData || !file) return
|
if (!fileData || !file) return
|
||||||
|
|
||||||
const parts = fileId.split("/")
|
const parts = fileId.split("/")
|
||||||
@ -468,11 +466,11 @@ export class FileManager {
|
|||||||
|
|
||||||
// Delete a file
|
// Delete a file
|
||||||
async deleteFile(fileId: string): Promise<(TFolder | TFile)[]> {
|
async deleteFile(fileId: string): Promise<(TFolder | TFile)[]> {
|
||||||
const file = this.sandboxFiles.fileData.find((f) => f.id === fileId)
|
const file = this.fileData.find((f) => f.id === fileId)
|
||||||
if (!file) return this.sandboxFiles.files
|
if (!file) return this.files
|
||||||
|
|
||||||
await this.sandbox.files.remove(path.posix.join(this.dirName, fileId))
|
await this.sandbox.files.remove(path.posix.join(this.dirName, fileId))
|
||||||
this.sandboxFiles.fileData = this.sandboxFiles.fileData.filter(
|
this.fileData = this.fileData.filter(
|
||||||
(f) => f.id !== fileId
|
(f) => f.id !== fileId
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -487,7 +485,7 @@ export class FileManager {
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
files.map(async (file) => {
|
files.map(async (file) => {
|
||||||
await this.sandbox.files.remove(path.posix.join(this.dirName, file))
|
await this.sandbox.files.remove(path.posix.join(this.dirName, file))
|
||||||
this.sandboxFiles.fileData = this.sandboxFiles.fileData.filter(
|
this.fileData = this.fileData.filter(
|
||||||
(f) => f.id !== file
|
(f) => f.id !== file
|
||||||
)
|
)
|
||||||
await RemoteFileStorage.deleteFile(this.getRemoteFileId(file))
|
await RemoteFileStorage.deleteFile(this.getRemoteFileId(file))
|
||||||
|
243
backend/server/src/Sandbox.ts
Normal file
243
backend/server/src/Sandbox.ts
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
import { Sandbox as E2BSandbox } from "e2b"
|
||||||
|
import { Socket } from "socket.io"
|
||||||
|
import { AIWorker } from "./AIWorker"
|
||||||
|
import { CONTAINER_TIMEOUT } from "./constants"
|
||||||
|
import { DokkuClient } from "./DokkuClient"
|
||||||
|
import { FileManager } from "./FileManager"
|
||||||
|
import {
|
||||||
|
createFileRL,
|
||||||
|
createFolderRL,
|
||||||
|
deleteFileRL,
|
||||||
|
renameFileRL,
|
||||||
|
saveFileRL,
|
||||||
|
} from "./ratelimit"
|
||||||
|
import { SecureGitClient } from "./SecureGitClient"
|
||||||
|
import { TerminalManager } from "./TerminalManager"
|
||||||
|
import { TFile, TFolder } from "./types"
|
||||||
|
import { LockManager } from "./utils"
|
||||||
|
|
||||||
|
const lockManager = new LockManager()
|
||||||
|
|
||||||
|
// Define a type for SocketHandler functions
|
||||||
|
type SocketHandler<T = Record<string, any>> = (args: T) => any;
|
||||||
|
|
||||||
|
// Extract port number from a string
|
||||||
|
function extractPortNumber(inputString: string): number | null {
|
||||||
|
const cleanedString = inputString.replace(/\x1B\[[0-9;]*m/g, "")
|
||||||
|
const regex = /http:\/\/localhost:(\d+)/
|
||||||
|
const match = cleanedString.match(regex)
|
||||||
|
return match ? parseInt(match[1]) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerContext = {
|
||||||
|
aiWorker: AIWorker;
|
||||||
|
dokkuClient: DokkuClient | null;
|
||||||
|
gitClient: SecureGitClient | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Sandbox {
|
||||||
|
// Sandbox properties:
|
||||||
|
sandboxId: string;
|
||||||
|
fileManager: FileManager | null;
|
||||||
|
terminalManager: TerminalManager | null;
|
||||||
|
container: E2BSandbox | null;
|
||||||
|
// Server context:
|
||||||
|
dokkuClient: DokkuClient | null;
|
||||||
|
gitClient: SecureGitClient | null;
|
||||||
|
aiWorker: AIWorker;
|
||||||
|
|
||||||
|
constructor(sandboxId: string, { aiWorker, dokkuClient, gitClient }: ServerContext) {
|
||||||
|
// Sandbox properties:
|
||||||
|
this.sandboxId = sandboxId;
|
||||||
|
this.fileManager = null;
|
||||||
|
this.terminalManager = null;
|
||||||
|
this.container = null;
|
||||||
|
// Server context:
|
||||||
|
this.aiWorker = aiWorker;
|
||||||
|
this.dokkuClient = dokkuClient;
|
||||||
|
this.gitClient = gitClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializes the container for the sandbox environment
|
||||||
|
async initialize(
|
||||||
|
fileWatchCallback: ((files: (TFolder | TFile)[]) => void) | undefined
|
||||||
|
) {
|
||||||
|
// Acquire a lock to ensure exclusive access to the sandbox environment
|
||||||
|
await lockManager.acquireLock(this.sandboxId, async () => {
|
||||||
|
// Check if a container already exists and is running
|
||||||
|
if (this.container && await this.container.isRunning()) {
|
||||||
|
console.log(`Found existing container ${this.sandboxId}`)
|
||||||
|
} else {
|
||||||
|
console.log("Creating container", this.sandboxId)
|
||||||
|
// Create a new container with a specified timeout
|
||||||
|
this.container = await E2BSandbox.create({
|
||||||
|
timeoutMs: CONTAINER_TIMEOUT,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Ensure a container was successfully created
|
||||||
|
if (!this.container) throw new Error("Failed to create container")
|
||||||
|
|
||||||
|
// Initialize the terminal manager if it hasn't been set up yet
|
||||||
|
if (!this.terminalManager) {
|
||||||
|
this.terminalManager = new TerminalManager(this.container)
|
||||||
|
console.log(`Terminal manager set up for ${this.sandboxId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the file manager if it hasn't been set up yet
|
||||||
|
if (!this.fileManager) {
|
||||||
|
this.fileManager = new FileManager(
|
||||||
|
this.sandboxId,
|
||||||
|
this.container,
|
||||||
|
fileWatchCallback ?? null
|
||||||
|
)
|
||||||
|
// Initialize the file manager and emit the initial files
|
||||||
|
await this.fileManager.initialize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when the client disconnects from the Sandbox
|
||||||
|
async disconnect() {
|
||||||
|
// Close all terminals managed by the terminal manager
|
||||||
|
await this.terminalManager?.closeAllTerminals()
|
||||||
|
// This way the terminal manager will be set up again if we reconnect
|
||||||
|
this.terminalManager = null;
|
||||||
|
// Close all file watchers managed by the file manager
|
||||||
|
await this.fileManager?.closeWatchers()
|
||||||
|
// This way the file manager will be set up again if we reconnect
|
||||||
|
this.fileManager = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlers(connection: { userId: string, isOwner: boolean, socket: Socket }) {
|
||||||
|
|
||||||
|
// Handle heartbeat from a socket connection
|
||||||
|
const handleHeartbeat: SocketHandler = (_: any) => {
|
||||||
|
// Only keep the sandbox alive if the owner is still connected
|
||||||
|
if (connection.isOwner) {
|
||||||
|
this.container?.setTimeout(CONTAINER_TIMEOUT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle getting a file
|
||||||
|
const handleGetFile: SocketHandler = ({ fileId }: any) => {
|
||||||
|
return this.fileManager?.getFile(fileId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle getting a folder
|
||||||
|
const handleGetFolder: SocketHandler = ({ folderId }: any) => {
|
||||||
|
return this.fileManager?.getFolder(folderId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle saving a file
|
||||||
|
const handleSaveFile: SocketHandler = async ({ fileId, body }: any) => {
|
||||||
|
await saveFileRL.consume(connection.userId, 1);
|
||||||
|
return this.fileManager?.saveFile(fileId, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle moving a file
|
||||||
|
const handleMoveFile: SocketHandler = ({ fileId, folderId }: any) => {
|
||||||
|
return this.fileManager?.moveFile(fileId, folderId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle listing apps
|
||||||
|
const handleListApps: SocketHandler = async (_: any) => {
|
||||||
|
if (!this.dokkuClient) throw Error("Failed to retrieve apps list: No Dokku client")
|
||||||
|
return { success: true, apps: await this.dokkuClient.listApps() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle deploying code
|
||||||
|
const handleDeploy: SocketHandler = async (_: any) => {
|
||||||
|
if (!this.gitClient) throw Error("No git client")
|
||||||
|
if (!this.fileManager) throw Error("No file manager")
|
||||||
|
await this.gitClient.pushFiles(this.fileManager?.fileData, this.sandboxId)
|
||||||
|
return { success: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle creating a file
|
||||||
|
const handleCreateFile: SocketHandler = async ({ name }: any) => {
|
||||||
|
await createFileRL.consume(connection.userId, 1);
|
||||||
|
return { "success": await this.fileManager?.createFile(name) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle creating a folder
|
||||||
|
const handleCreateFolder: SocketHandler = async ({ name }: any) => {
|
||||||
|
await createFolderRL.consume(connection.userId, 1);
|
||||||
|
return { "success": await this.fileManager?.createFolder(name) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle renaming a file
|
||||||
|
const handleRenameFile: SocketHandler = async ({ fileId, newName }: any) => {
|
||||||
|
await renameFileRL.consume(connection.userId, 1)
|
||||||
|
return this.fileManager?.renameFile(fileId, newName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle deleting a file
|
||||||
|
const handleDeleteFile: SocketHandler = async ({ fileId }: any) => {
|
||||||
|
await deleteFileRL.consume(connection.userId, 1)
|
||||||
|
return this.fileManager?.deleteFile(fileId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle deleting a folder
|
||||||
|
const handleDeleteFolder: SocketHandler = ({ folderId }: any) => {
|
||||||
|
return this.fileManager?.deleteFolder(folderId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle creating a terminal session
|
||||||
|
const handleCreateTerminal: SocketHandler = async ({ id }: any) => {
|
||||||
|
await lockManager.acquireLock(this.sandboxId, async () => {
|
||||||
|
await this.terminalManager?.createTerminal(id, (responseString: string) => {
|
||||||
|
connection.socket.emit("terminalResponse", { id, data: responseString })
|
||||||
|
const port = extractPortNumber(responseString)
|
||||||
|
if (port) {
|
||||||
|
connection.socket.emit(
|
||||||
|
"previewURL",
|
||||||
|
"https://" + this.container?.getHost(port)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle resizing a terminal
|
||||||
|
const handleResizeTerminal: SocketHandler = ({ dimensions }: any) => {
|
||||||
|
this.terminalManager?.resizeTerminal(dimensions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle sending data to a terminal
|
||||||
|
const handleTerminalData: SocketHandler = ({ id, data }: any) => {
|
||||||
|
return this.terminalManager?.sendTerminalData(id, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle closing a terminal
|
||||||
|
const handleCloseTerminal: SocketHandler = ({ id }: any) => {
|
||||||
|
return this.terminalManager?.closeTerminal(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle generating code
|
||||||
|
const handleGenerateCode: SocketHandler = ({ fileName, code, line, instructions }: any) => {
|
||||||
|
return this.aiWorker.generateCode(connection.userId, fileName, code, line, instructions)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"heartbeat": handleHeartbeat,
|
||||||
|
"getFile": handleGetFile,
|
||||||
|
"getFolder": handleGetFolder,
|
||||||
|
"saveFile": handleSaveFile,
|
||||||
|
"moveFile": handleMoveFile,
|
||||||
|
"list": handleListApps,
|
||||||
|
"deploy": handleDeploy,
|
||||||
|
"createFile": handleCreateFile,
|
||||||
|
"createFolder": handleCreateFolder,
|
||||||
|
"renameFile": handleRenameFile,
|
||||||
|
"deleteFile": handleDeleteFile,
|
||||||
|
"deleteFolder": handleDeleteFolder,
|
||||||
|
"createTerminal": handleCreateTerminal,
|
||||||
|
"resizeTerminal": handleResizeTerminal,
|
||||||
|
"terminalData": handleTerminalData,
|
||||||
|
"closeTerminal": handleCloseTerminal,
|
||||||
|
"generateCode": handleGenerateCode,
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
2
backend/server/src/constants.ts
Normal file
2
backend/server/src/constants.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// The amount of time in ms that a container will stay alive without a hearbeat.
|
||||||
|
export const CONTAINER_TIMEOUT = 120_000
|
@ -1,42 +1,39 @@
|
|||||||
import cors from "cors"
|
import cors from "cors"
|
||||||
import dotenv from "dotenv"
|
import dotenv from "dotenv"
|
||||||
import { Sandbox } from "e2b"
|
|
||||||
import express, { Express } from "express"
|
import express, { Express } from "express"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import { createServer } from "http"
|
import { createServer } from "http"
|
||||||
import { Server } from "socket.io"
|
import { Server, Socket } from "socket.io"
|
||||||
import { z } from "zod"
|
|
||||||
import { AIWorker } from "./AIWorker"
|
import { AIWorker } from "./AIWorker"
|
||||||
|
|
||||||
|
import { ConnectionManager } from "./ConnectionManager"
|
||||||
import { DokkuClient } from "./DokkuClient"
|
import { DokkuClient } from "./DokkuClient"
|
||||||
import { FileManager, SandboxFiles } from "./FileManager"
|
import { Sandbox } from "./Sandbox"
|
||||||
import {
|
|
||||||
createFileRL,
|
|
||||||
createFolderRL,
|
|
||||||
deleteFileRL,
|
|
||||||
renameFileRL,
|
|
||||||
saveFileRL,
|
|
||||||
} from "./ratelimit"
|
|
||||||
import { SecureGitClient } from "./SecureGitClient"
|
import { SecureGitClient } from "./SecureGitClient"
|
||||||
import { TerminalManager } from "./TerminalManager"
|
import { socketAuth } from "./socketAuth"; // Import the new socketAuth middleware
|
||||||
import { User } from "./types"
|
import { TFile, TFolder } from "./types"
|
||||||
import { LockManager } from "./utils"
|
|
||||||
|
// Log errors and send a notification to the client
|
||||||
|
export const handleErrors = (message: string, error: any, socket: Socket) => {
|
||||||
|
console.error(message, error);
|
||||||
|
socket.emit("error", `${message} ${error.message ?? error}`);
|
||||||
|
};
|
||||||
|
|
||||||
// Handle uncaught exceptions
|
// Handle uncaught exceptions
|
||||||
process.on("uncaughtException", (error) => {
|
process.on("uncaughtException", (error) => {
|
||||||
console.error("Uncaught Exception:", error)
|
console.error("Uncaught Exception:", error)
|
||||||
// Do not exit the process
|
// Do not exit the process
|
||||||
// You can add additional logging or recovery logic here
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Handle unhandled promise rejections
|
// Handle unhandled promise rejections
|
||||||
process.on("unhandledRejection", (reason, promise) => {
|
process.on("unhandledRejection", (reason, promise) => {
|
||||||
console.error("Unhandled Rejection at:", promise, "reason:", reason)
|
console.error("Unhandled Rejection at:", promise, "reason:", reason)
|
||||||
// Do not exit the process
|
// Do not exit the process
|
||||||
// You can also handle the rejected promise here if needed
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// The amount of time in ms that a container will stay alive without a hearbeat.
|
// Initialize containers and managers
|
||||||
const CONTAINER_TIMEOUT = 120_000
|
const connections = new ConnectionManager()
|
||||||
|
const sandboxes: Record<string, Sandbox> = {}
|
||||||
|
|
||||||
// Load environment variables
|
// Load environment variables
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
@ -48,102 +45,23 @@ app.use(cors())
|
|||||||
const httpServer = createServer(app)
|
const httpServer = createServer(app)
|
||||||
const io = new Server(httpServer, {
|
const io = new Server(httpServer, {
|
||||||
cors: {
|
cors: {
|
||||||
origin: "*",
|
origin: "*", // Allow connections from any origin
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Check if the sandbox owner is connected
|
|
||||||
function isOwnerConnected(sandboxId: string): boolean {
|
|
||||||
return (connections[sandboxId] ?? 0) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract port number from a string
|
|
||||||
function extractPortNumber(inputString: string): number | null {
|
|
||||||
const cleanedString = inputString.replace(/\x1B\[[0-9;]*m/g, "")
|
|
||||||
const regex = /http:\/\/localhost:(\d+)/
|
|
||||||
const match = cleanedString.match(regex)
|
|
||||||
return match ? parseInt(match[1]) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize containers and managers
|
|
||||||
const containers: Record<string, Sandbox> = {}
|
|
||||||
const connections: Record<string, number> = {}
|
|
||||||
const fileManagers: Record<string, FileManager> = {}
|
|
||||||
const terminalManagers: Record<string, TerminalManager> = {}
|
|
||||||
|
|
||||||
// Middleware for socket authentication
|
// Middleware for socket authentication
|
||||||
io.use(async (socket, next) => {
|
io.use(socketAuth) // Use the new socketAuth middleware
|
||||||
// Define the schema for handshake query validation
|
|
||||||
const handshakeSchema = z.object({
|
|
||||||
userId: z.string(),
|
|
||||||
sandboxId: z.string(),
|
|
||||||
EIO: z.string(),
|
|
||||||
transport: z.string(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const q = socket.handshake.query
|
|
||||||
const parseQuery = handshakeSchema.safeParse(q)
|
|
||||||
|
|
||||||
// Check if the query is valid according to the schema
|
|
||||||
if (!parseQuery.success) {
|
|
||||||
next(new Error("Invalid request."))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const { sandboxId, userId } = parseQuery.data
|
|
||||||
// Fetch user data from the database
|
|
||||||
const dbUser = await fetch(
|
|
||||||
`${process.env.DATABASE_WORKER_URL}/api/user?id=${userId}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `${process.env.WORKERS_KEY}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const dbUserJSON = (await dbUser.json()) as User
|
|
||||||
|
|
||||||
// Check if user data was retrieved successfully
|
|
||||||
if (!dbUserJSON) {
|
|
||||||
next(new Error("DB error."))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the user owns the sandbox or has shared access
|
|
||||||
const sandbox = dbUserJSON.sandbox.find((s) => s.id === sandboxId)
|
|
||||||
const sharedSandboxes = dbUserJSON.usersToSandboxes.find(
|
|
||||||
(uts) => uts.sandboxId === sandboxId
|
|
||||||
)
|
|
||||||
|
|
||||||
// If user doesn't own or have shared access to the sandbox, deny access
|
|
||||||
if (!sandbox && !sharedSandboxes) {
|
|
||||||
next(new Error("Invalid credentials."))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set socket data with user information
|
|
||||||
socket.data = {
|
|
||||||
userId,
|
|
||||||
sandboxId: sandboxId,
|
|
||||||
isOwner: sandbox !== undefined,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow the connection
|
|
||||||
next()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Initialize lock manager
|
|
||||||
const lockManager = new LockManager()
|
|
||||||
|
|
||||||
// Check for required environment variables
|
// Check for required environment variables
|
||||||
if (!process.env.DOKKU_HOST)
|
if (!process.env.DOKKU_HOST)
|
||||||
console.error("Environment variable DOKKU_HOST is not defined")
|
console.warn("Environment variable DOKKU_HOST is not defined")
|
||||||
if (!process.env.DOKKU_USERNAME)
|
if (!process.env.DOKKU_USERNAME)
|
||||||
console.error("Environment variable DOKKU_USERNAME is not defined")
|
console.warn("Environment variable DOKKU_USERNAME is not defined")
|
||||||
if (!process.env.DOKKU_KEY)
|
if (!process.env.DOKKU_KEY)
|
||||||
console.error("Environment variable DOKKU_KEY is not defined")
|
console.warn("Environment variable DOKKU_KEY is not defined")
|
||||||
|
|
||||||
// Initialize Dokku client
|
// Initialize Dokku client
|
||||||
const client =
|
const dokkuClient =
|
||||||
process.env.DOKKU_HOST && process.env.DOKKU_KEY && process.env.DOKKU_USERNAME
|
process.env.DOKKU_HOST && process.env.DOKKU_KEY && process.env.DOKKU_USERNAME
|
||||||
? new DokkuClient({
|
? new DokkuClient({
|
||||||
host: process.env.DOKKU_HOST,
|
host: process.env.DOKKU_HOST,
|
||||||
@ -151,10 +69,10 @@ const client =
|
|||||||
privateKey: fs.readFileSync(process.env.DOKKU_KEY),
|
privateKey: fs.readFileSync(process.env.DOKKU_KEY),
|
||||||
})
|
})
|
||||||
: null
|
: null
|
||||||
client?.connect()
|
dokkuClient?.connect()
|
||||||
|
|
||||||
// Initialize Git client used to deploy Dokku apps
|
// Initialize Git client used to deploy Dokku apps
|
||||||
const git =
|
const gitClient =
|
||||||
process.env.DOKKU_HOST && process.env.DOKKU_KEY
|
process.env.DOKKU_HOST && process.env.DOKKU_KEY
|
||||||
? new SecureGitClient(
|
? new SecureGitClient(
|
||||||
`dokku@${process.env.DOKKU_HOST}`,
|
`dokku@${process.env.DOKKU_HOST}`,
|
||||||
@ -170,353 +88,91 @@ const aiWorker = new AIWorker(
|
|||||||
process.env.WORKERS_KEY!
|
process.env.WORKERS_KEY!
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handle socket connections
|
// Handle a client connecting to the server
|
||||||
io.on("connection", async (socket) => {
|
io.on("connection", async (socket) => {
|
||||||
try {
|
try {
|
||||||
|
// This data comes is added by our authentication middleware
|
||||||
const data = socket.data as {
|
const data = socket.data as {
|
||||||
userId: string
|
userId: string
|
||||||
sandboxId: string
|
sandboxId: string
|
||||||
isOwner: boolean
|
isOwner: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle connection based on user type (owner or not)
|
// Register the connection
|
||||||
if (data.isOwner) {
|
connections.addConnectionForSandbox(socket, data.sandboxId, data.isOwner)
|
||||||
connections[data.sandboxId] = (connections[data.sandboxId] ?? 0) + 1
|
|
||||||
} else {
|
// Disable access unless the sandbox owner is connected
|
||||||
if (!isOwnerConnected(data.sandboxId)) {
|
if (!data.isOwner && !connections.ownerIsConnected(data.sandboxId)) {
|
||||||
socket.emit("disableAccess", "The sandbox owner is not connected.")
|
socket.emit("disableAccess", "The sandbox owner is not connected.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Create or retrieve container
|
try {
|
||||||
const createdContainer = await lockManager.acquireLock(
|
// Create or retrieve the sandbox manager for the given sandbox ID
|
||||||
|
const sandbox = sandboxes[data.sandboxId] ?? new Sandbox(
|
||||||
data.sandboxId,
|
data.sandboxId,
|
||||||
async () => {
|
{
|
||||||
try {
|
aiWorker, dokkuClient, gitClient,
|
||||||
// Start a new container if the container doesn't exist or it timed out.
|
|
||||||
if (
|
|
||||||
!containers[data.sandboxId] ||
|
|
||||||
!(await containers[data.sandboxId].isRunning())
|
|
||||||
) {
|
|
||||||
containers[data.sandboxId] = await Sandbox.create({
|
|
||||||
timeoutMs: CONTAINER_TIMEOUT,
|
|
||||||
})
|
|
||||||
console.log("Created container ", data.sandboxId)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(`Error creating container ${data.sandboxId}:`, e)
|
|
||||||
io.emit("error", `Error: container creation. ${e.message ?? e}`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
sandboxes[data.sandboxId] = sandbox
|
||||||
|
|
||||||
// Function to send loaded event
|
// This callback recieves an update when the file list changes, and notifies all relevant connections.
|
||||||
const sendLoadedEvent = (files: SandboxFiles) => {
|
const sendFileNotifications = (files: (TFolder | TFile)[]) => {
|
||||||
socket.emit("loaded", files.files)
|
connections.connectionsForSandbox(data.sandboxId).forEach((socket: Socket) => {
|
||||||
}
|
socket.emit("loaded", files);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize file and terminal managers if container was created
|
// Initialize the sandbox container
|
||||||
if (createdContainer) {
|
// The file manager and terminal managers will be set up if they have been closed
|
||||||
fileManagers[data.sandboxId] = new FileManager(
|
await sandbox.initialize(sendFileNotifications)
|
||||||
data.sandboxId,
|
socket.emit("loaded", sandbox.fileManager?.files)
|
||||||
containers[data.sandboxId],
|
|
||||||
sendLoadedEvent
|
|
||||||
)
|
|
||||||
await fileManagers[data.sandboxId].initialize()
|
|
||||||
terminalManagers[data.sandboxId] = new TerminalManager(
|
|
||||||
containers[data.sandboxId]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileManager = fileManagers[data.sandboxId]
|
// Register event handlers for the sandbox
|
||||||
const terminalManager = terminalManagers[data.sandboxId]
|
// For each event handler, listen on the socket for that event
|
||||||
|
// Pass connection-specific information to the handlers
|
||||||
// Load file list from the file manager into the editor
|
Object.entries(sandbox.handlers({
|
||||||
sendLoadedEvent(fileManager.sandboxFiles)
|
userId: data.userId,
|
||||||
|
isOwner: data.isOwner,
|
||||||
// Handle various socket events (heartbeat, file operations, terminal operations, etc.)
|
socket
|
||||||
socket.on("heartbeat", async () => {
|
})).forEach(([event, handler]) => {
|
||||||
|
socket.on(event, async (options: any, callback?: (response: any) => void) => {
|
||||||
try {
|
try {
|
||||||
// This keeps the container alive for another CONTAINER_TIMEOUT seconds.
|
const result = await handler(options)
|
||||||
// The E2B docs are unclear, but the timeout is relative to the time of this method call.
|
callback?.(result);
|
||||||
await containers[data.sandboxId].setTimeout(CONTAINER_TIMEOUT)
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Error setting timeout:", e)
|
handleErrors(`Error processing event "${event}":`, e, socket);
|
||||||
io.emit("error", `Error: set timeout. ${e.message ?? e}`)
|
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Handle request to get file content
|
// Handle disconnection event
|
||||||
socket.on("getFile", async (fileId: string, callback) => {
|
|
||||||
try {
|
|
||||||
const fileContent = await fileManager.getFile(fileId)
|
|
||||||
callback(fileContent)
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error("Error getting file:", e)
|
|
||||||
io.emit("error", `Error: get file. ${e.message ?? e}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle request to get folder contents
|
|
||||||
socket.on("getFolder", async (folderId: string, callback) => {
|
|
||||||
try {
|
|
||||||
const files = await fileManager.getFolder(folderId)
|
|
||||||
callback(files)
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error("Error getting folder:", e)
|
|
||||||
io.emit("error", `Error: get folder. ${e.message ?? e}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle request to save file
|
|
||||||
socket.on("saveFile", async (fileId: string, body: string) => {
|
|
||||||
try {
|
|
||||||
await saveFileRL.consume(data.userId, 1)
|
|
||||||
await fileManager.saveFile(fileId, body)
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error("Error saving file:", e)
|
|
||||||
io.emit("error", `Error: file saving. ${e.message ?? e}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle request to move file
|
|
||||||
socket.on(
|
|
||||||
"moveFile",
|
|
||||||
async (fileId: string, folderId: string, callback) => {
|
|
||||||
try {
|
|
||||||
const newFiles = await fileManager.moveFile(fileId, folderId)
|
|
||||||
callback(newFiles)
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error("Error moving file:", e)
|
|
||||||
io.emit("error", `Error: file moving. ${e.message ?? e}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
interface CallbackResponse {
|
|
||||||
success: boolean
|
|
||||||
apps?: string[]
|
|
||||||
message?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle request to list apps
|
|
||||||
socket.on(
|
|
||||||
"list",
|
|
||||||
async (callback: (response: CallbackResponse) => void) => {
|
|
||||||
console.log("Retrieving apps list...")
|
|
||||||
try {
|
|
||||||
if (!client)
|
|
||||||
throw Error("Failed to retrieve apps list: No Dokku client")
|
|
||||||
callback({
|
|
||||||
success: true,
|
|
||||||
apps: await client.listApps(),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
callback({
|
|
||||||
success: false,
|
|
||||||
message: "Failed to retrieve apps list",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Handle request to deploy project
|
|
||||||
socket.on(
|
|
||||||
"deploy",
|
|
||||||
async (callback: (response: CallbackResponse) => void) => {
|
|
||||||
try {
|
|
||||||
// Push the project files to the Dokku server
|
|
||||||
console.log("Deploying project ${data.sandboxId}...")
|
|
||||||
if (!git) throw Error("Failed to retrieve apps list: No git client")
|
|
||||||
// Remove the /project/[id]/ component of each file path:
|
|
||||||
const fixedFilePaths = fileManager.sandboxFiles.fileData.map(
|
|
||||||
(file) => {
|
|
||||||
return {
|
|
||||||
...file,
|
|
||||||
id: file.id.split("/").slice(2).join("/"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
// Push all files to Dokku.
|
|
||||||
await git.pushFiles(fixedFilePaths, data.sandboxId)
|
|
||||||
callback({
|
|
||||||
success: true,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
callback({
|
|
||||||
success: false,
|
|
||||||
message: "Failed to deploy project: " + error,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Handle request to create a new file
|
|
||||||
socket.on("createFile", async (name: string, callback) => {
|
|
||||||
try {
|
|
||||||
await createFileRL.consume(data.userId, 1)
|
|
||||||
const success = await fileManager.createFile(name)
|
|
||||||
callback({ success })
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error("Error creating file:", e)
|
|
||||||
io.emit("error", `Error: file creation. ${e.message ?? e}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle request to create a new folder
|
|
||||||
socket.on("createFolder", async (name: string, callback) => {
|
|
||||||
try {
|
|
||||||
await createFolderRL.consume(data.userId, 1)
|
|
||||||
await fileManager.createFolder(name)
|
|
||||||
callback()
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error("Error creating folder:", e)
|
|
||||||
io.emit("error", `Error: folder creation. ${e.message ?? e}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle request to rename a file
|
|
||||||
socket.on("renameFile", async (fileId: string, newName: string) => {
|
|
||||||
try {
|
|
||||||
await renameFileRL.consume(data.userId, 1)
|
|
||||||
await fileManager.renameFile(fileId, newName)
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error("Error renaming file:", e)
|
|
||||||
io.emit("error", `Error: file renaming. ${e.message ?? e}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle request to delete a file
|
|
||||||
socket.on("deleteFile", async (fileId: string, callback) => {
|
|
||||||
try {
|
|
||||||
await deleteFileRL.consume(data.userId, 1)
|
|
||||||
const newFiles = await fileManager.deleteFile(fileId)
|
|
||||||
callback(newFiles)
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error("Error deleting file:", e)
|
|
||||||
io.emit("error", `Error: file deletion. ${e.message ?? e}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle request to delete a folder
|
|
||||||
socket.on("deleteFolder", async (folderId: string, callback) => {
|
|
||||||
try {
|
|
||||||
const newFiles = await fileManager.deleteFolder(folderId)
|
|
||||||
callback(newFiles)
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error("Error deleting folder:", e)
|
|
||||||
io.emit("error", `Error: folder deletion. ${e.message ?? e}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle request to create a new terminal
|
|
||||||
socket.on("createTerminal", async (id: string, callback) => {
|
|
||||||
try {
|
|
||||||
await lockManager.acquireLock(data.sandboxId, async () => {
|
|
||||||
await terminalManager.createTerminal(id, (responseString: string) => {
|
|
||||||
io.emit("terminalResponse", { id, data: responseString })
|
|
||||||
const port = extractPortNumber(responseString)
|
|
||||||
if (port) {
|
|
||||||
io.emit(
|
|
||||||
"previewURL",
|
|
||||||
"https://" + containers[data.sandboxId].getHost(port)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
callback()
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(`Error creating terminal ${id}:`, e)
|
|
||||||
io.emit("error", `Error: terminal creation. ${e.message ?? e}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle request to resize terminal
|
|
||||||
socket.on(
|
|
||||||
"resizeTerminal",
|
|
||||||
(dimensions: { cols: number; rows: number }) => {
|
|
||||||
try {
|
|
||||||
terminalManager.resizeTerminal(dimensions)
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error("Error resizing terminal:", e)
|
|
||||||
io.emit("error", `Error: terminal resizing. ${e.message ?? e}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Handle terminal input data
|
|
||||||
socket.on("terminalData", async (id: string, data: string) => {
|
|
||||||
try {
|
|
||||||
await terminalManager.sendTerminalData(id, data)
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error("Error writing to terminal:", e)
|
|
||||||
io.emit("error", `Error: writing to terminal. ${e.message ?? e}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle request to close terminal
|
|
||||||
socket.on("closeTerminal", async (id: string, callback) => {
|
|
||||||
try {
|
|
||||||
await terminalManager.closeTerminal(id)
|
|
||||||
callback()
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error("Error closing terminal:", e)
|
|
||||||
io.emit("error", `Error: closing terminal. ${e.message ?? e}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle request to generate code
|
|
||||||
socket.on(
|
|
||||||
"generateCode",
|
|
||||||
async (
|
|
||||||
fileName: string,
|
|
||||||
code: string,
|
|
||||||
line: number,
|
|
||||||
instructions: string,
|
|
||||||
callback
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const result = await aiWorker.generateCode(
|
|
||||||
data.userId,
|
|
||||||
fileName,
|
|
||||||
code,
|
|
||||||
line,
|
|
||||||
instructions
|
|
||||||
)
|
|
||||||
callback(result)
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error("Error generating code:", e)
|
|
||||||
io.emit("error", `Error: code generation. ${e.message ?? e}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Handle socket disconnection
|
|
||||||
socket.on("disconnect", async () => {
|
socket.on("disconnect", async () => {
|
||||||
try {
|
try {
|
||||||
if (data.isOwner) {
|
// Deregister the connection
|
||||||
connections[data.sandboxId]--
|
connections.removeConnectionForSandbox(socket, data.sandboxId, data.isOwner)
|
||||||
}
|
|
||||||
|
|
||||||
await terminalManager.closeAllTerminals()
|
// If the owner has disconnected from all sockets, close open terminals and file watchers.o
|
||||||
await fileManager.closeWatchers()
|
// The sandbox itself will timeout after the heartbeat stops.
|
||||||
|
if (data.isOwner && !connections.ownerIsConnected(data.sandboxId)) {
|
||||||
if (data.isOwner && connections[data.sandboxId] <= 0) {
|
await sandbox.disconnect()
|
||||||
socket.broadcast.emit(
|
socket.broadcast.emit(
|
||||||
"disableAccess",
|
"disableAccess",
|
||||||
"The sandbox owner has disconnected."
|
"The sandbox owner has disconnected."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.log("Error disconnecting:", e)
|
handleErrors("Error disconnecting:", e, socket);
|
||||||
io.emit("error", `Error: disconnecting. ${e.message ?? e}`)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Error connecting:", e)
|
handleErrors(`Error initializing sandbox ${data.sandboxId}:`, e, socket);
|
||||||
io.emit("error", `Error: connection. ${e.message ?? e}`)
|
}
|
||||||
|
|
||||||
|
} catch (e: any) {
|
||||||
|
handleErrors("Error connecting:", e, socket);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
63
backend/server/src/socketAuth.ts
Normal file
63
backend/server/src/socketAuth.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { Socket } from "socket.io"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { User } from "./types"
|
||||||
|
|
||||||
|
// Middleware for socket authentication
|
||||||
|
export const socketAuth = async (socket: Socket, next: Function) => {
|
||||||
|
// Define the schema for handshake query validation
|
||||||
|
const handshakeSchema = z.object({
|
||||||
|
userId: z.string(),
|
||||||
|
sandboxId: z.string(),
|
||||||
|
EIO: z.string(),
|
||||||
|
transport: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const q = socket.handshake.query
|
||||||
|
const parseQuery = handshakeSchema.safeParse(q)
|
||||||
|
|
||||||
|
// Check if the query is valid according to the schema
|
||||||
|
if (!parseQuery.success) {
|
||||||
|
next(new Error("Invalid request."))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sandboxId, userId } = parseQuery.data
|
||||||
|
// Fetch user data from the database
|
||||||
|
const dbUser = await fetch(
|
||||||
|
`${process.env.DATABASE_WORKER_URL}/api/user?id=${userId}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `${process.env.WORKERS_KEY}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const dbUserJSON = (await dbUser.json()) as User
|
||||||
|
|
||||||
|
// Check if user data was retrieved successfully
|
||||||
|
if (!dbUserJSON) {
|
||||||
|
next(new Error("DB error."))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user owns the sandbox or has shared access
|
||||||
|
const sandbox = dbUserJSON.sandbox.find((s) => s.id === sandboxId)
|
||||||
|
const sharedSandboxes = dbUserJSON.usersToSandboxes.find(
|
||||||
|
(uts) => uts.sandboxId === sandboxId
|
||||||
|
)
|
||||||
|
|
||||||
|
// If user doesn't own or have shared access to the sandbox, deny access
|
||||||
|
if (!sandbox && !sharedSandboxes) {
|
||||||
|
next(new Error("Invalid credentials."))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set socket data with user information
|
||||||
|
socket.data = {
|
||||||
|
userId,
|
||||||
|
sandboxId: sandboxId,
|
||||||
|
isOwner: sandbox !== undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow the connection
|
||||||
|
next()
|
||||||
|
}
|
@ -68,3 +68,8 @@ export type R2FileBody = R2FileData & {
|
|||||||
json: Promise<any>
|
json: Promise<any>
|
||||||
blob: Promise<Blob>
|
blob: Promise<Blob>
|
||||||
}
|
}
|
||||||
|
export interface DokkuResponse {
|
||||||
|
success: boolean
|
||||||
|
apps?: string[]
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"tabWidth": 2,
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": false
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
|
import pLimit from "p-limit"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import pLimit from 'p-limit';
|
|
||||||
|
|
||||||
export interface Env {
|
export interface Env {
|
||||||
R2: R2Bucket
|
R2: R2Bucket
|
||||||
@ -144,20 +144,24 @@ export default {
|
|||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const { sandboxId, type } = initSchema.parse(body)
|
const { sandboxId, type } = initSchema.parse(body)
|
||||||
|
|
||||||
console.log(`Copying template: ${type}`);
|
console.log(`Copying template: ${type}`)
|
||||||
|
|
||||||
// List all objects under the directory
|
// List all objects under the directory
|
||||||
const { objects } = await env.Templates.list({ prefix: type });
|
const { objects } = await env.Templates.list({ prefix: type })
|
||||||
|
|
||||||
// Copy each object to the new directory with a 5 concurrency limit
|
// Copy each object to the new directory with a 5 concurrency limit
|
||||||
const limit = pLimit(5);
|
const limit = pLimit(5)
|
||||||
await Promise.all(objects.map(({ key }) =>
|
await Promise.all(
|
||||||
|
objects.map(({ key }) =>
|
||||||
limit(async () => {
|
limit(async () => {
|
||||||
const destinationKey = key.replace(type, `projects/${sandboxId}`);
|
const destinationKey = key.replace(type, `projects/${sandboxId}`)
|
||||||
const fileBody = await env.Templates.get(key).then(res => res?.body ?? "");
|
const fileBody = await env.Templates.get(key).then(
|
||||||
await env.R2.put(destinationKey, fileBody);
|
(res) => res?.body ?? ""
|
||||||
|
)
|
||||||
|
await env.R2.put(destinationKey, fileBody)
|
||||||
})
|
})
|
||||||
));
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return success
|
return success
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,25 +1,30 @@
|
|||||||
// test/index.spec.ts
|
// test/index.spec.ts
|
||||||
import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test';
|
import {
|
||||||
import { describe, it, expect } from 'vitest';
|
createExecutionContext,
|
||||||
import worker from '../src/index';
|
env,
|
||||||
|
SELF,
|
||||||
|
waitOnExecutionContext,
|
||||||
|
} from "cloudflare:test"
|
||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
import worker from "../src/index"
|
||||||
|
|
||||||
// For now, you'll need to do something like this to get a correctly-typed
|
// For now, you'll need to do something like this to get a correctly-typed
|
||||||
// `Request` to pass to `worker.fetch()`.
|
// `Request` to pass to `worker.fetch()`.
|
||||||
const IncomingRequest = Request<unknown, IncomingRequestCfProperties>;
|
const IncomingRequest = Request<unknown, IncomingRequestCfProperties>
|
||||||
|
|
||||||
describe('Hello World worker', () => {
|
describe("Hello World worker", () => {
|
||||||
it('responds with Hello World! (unit style)', async () => {
|
it("responds with Hello World! (unit style)", async () => {
|
||||||
const request = new IncomingRequest('http://example.com');
|
const request = new IncomingRequest("http://example.com")
|
||||||
// Create an empty context to pass to `worker.fetch()`.
|
// Create an empty context to pass to `worker.fetch()`.
|
||||||
const ctx = createExecutionContext();
|
const ctx = createExecutionContext()
|
||||||
const response = await worker.fetch(request, env, ctx);
|
const response = await worker.fetch(request, env, ctx)
|
||||||
// Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
|
// Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
|
||||||
await waitOnExecutionContext(ctx);
|
await waitOnExecutionContext(ctx)
|
||||||
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
|
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`)
|
||||||
});
|
})
|
||||||
|
|
||||||
it('responds with Hello World! (integration style)', async () => {
|
it("responds with Hello World! (integration style)", async () => {
|
||||||
const response = await SELF.fetch('https://example.com');
|
const response = await SELF.fetch("https://example.com")
|
||||||
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
|
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`)
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
@ -12,7 +12,9 @@
|
|||||||
|
|
||||||
/* Language and Environment */
|
/* Language and Environment */
|
||||||
"target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
"target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||||
"lib": ["es2021"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
|
"lib": [
|
||||||
|
"es2021"
|
||||||
|
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
|
||||||
"jsx": "react" /* Specify what JSX code is generated. */,
|
"jsx": "react" /* Specify what JSX code is generated. */,
|
||||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
|
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"
|
||||||
|
|
||||||
export default defineWorkersConfig({
|
export default defineWorkersConfig({
|
||||||
test: {
|
test: {
|
||||||
@ -8,4 +8,4 @@ export default defineWorkersConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
3
backend/storage/worker-configuration.d.ts
vendored
3
backend/storage/worker-configuration.d.ts
vendored
@ -1,4 +1,3 @@
|
|||||||
// Generated by Wrangler
|
// Generated by Wrangler
|
||||||
// After adding bindings to `wrangler.toml`, regenerate this interface via `npm run cf-typegen`
|
// After adding bindings to `wrangler.toml`, regenerate this interface via `npm run cf-typegen`
|
||||||
interface Env {
|
interface Env {}
|
||||||
}
|
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"tabWidth": 2,
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": false
|
|
||||||
}
|
|
@ -1,12 +1,11 @@
|
|||||||
import Navbar from "@/components/editor/navbar"
|
|
||||||
import { Room } from "@/components/editor/live/room"
|
import { Room } from "@/components/editor/live/room"
|
||||||
|
import Loading from "@/components/editor/loading"
|
||||||
|
import Navbar from "@/components/editor/navbar"
|
||||||
|
import { TerminalProvider } from "@/context/TerminalContext"
|
||||||
import { Sandbox, User, UsersToSandboxes } from "@/lib/types"
|
import { Sandbox, User, UsersToSandboxes } from "@/lib/types"
|
||||||
import { currentUser } from "@clerk/nextjs"
|
import { currentUser } from "@clerk/nextjs"
|
||||||
import { notFound, redirect } from "next/navigation"
|
|
||||||
import Loading from "@/components/editor/loading"
|
|
||||||
import dynamic from "next/dynamic"
|
import dynamic from "next/dynamic"
|
||||||
import fs from "fs"
|
import { notFound, redirect } from "next/navigation"
|
||||||
import { TerminalProvider } from "@/context/TerminalContext"
|
|
||||||
|
|
||||||
export const revalidate = 0
|
export const revalidate = 0
|
||||||
|
|
||||||
@ -92,12 +91,13 @@ export default async function CodePage({ params }: { params: { id: string } }) {
|
|||||||
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
|
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
|
||||||
<Room id={sandboxId}>
|
<Room id={sandboxId}>
|
||||||
<TerminalProvider>
|
<TerminalProvider>
|
||||||
<Navbar userData={userData} sandboxData={sandboxData} shared={shared} />
|
<Navbar
|
||||||
<div className="w-screen flex grow">
|
|
||||||
<CodeEditor
|
|
||||||
userData={userData}
|
userData={userData}
|
||||||
sandboxData={sandboxData}
|
sandboxData={sandboxData}
|
||||||
|
shared={shared}
|
||||||
/>
|
/>
|
||||||
|
<div className="w-screen flex grow">
|
||||||
|
<CodeEditor userData={userData} sandboxData={sandboxData} />
|
||||||
</div>
|
</div>
|
||||||
</TerminalProvider>
|
</TerminalProvider>
|
||||||
</Room>
|
</Room>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { UserButton, currentUser } from "@clerk/nextjs"
|
|
||||||
import { redirect } from "next/navigation"
|
|
||||||
import Dashboard from "@/components/dashboard"
|
import Dashboard from "@/components/dashboard"
|
||||||
import Navbar from "@/components/dashboard/navbar"
|
import Navbar from "@/components/dashboard/navbar"
|
||||||
import { Sandbox, User } from "@/lib/types"
|
import { User } from "@/lib/types"
|
||||||
|
import { currentUser } from "@clerk/nextjs"
|
||||||
|
import { redirect } from "next/navigation"
|
||||||
|
|
||||||
export default async function DashboardPage() {
|
export default async function DashboardPage() {
|
||||||
const user = await currentUser()
|
const user = await currentUser()
|
||||||
|
@ -15,7 +15,7 @@ export const metadata: Metadata = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}>) {
|
}>) {
|
||||||
@ -29,9 +29,7 @@ export default function RootLayout({
|
|||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
<SocketProvider>
|
<SocketProvider>
|
||||||
<PreviewProvider>
|
<PreviewProvider>{children}</PreviewProvider>
|
||||||
{children}
|
|
||||||
</PreviewProvider>
|
|
||||||
</SocketProvider>
|
</SocketProvider>
|
||||||
<Analytics />
|
<Analytics />
|
||||||
<Toaster position="bottom-left" richColors />
|
<Toaster position="bottom-left" richColors />
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { currentUser } from "@clerk/nextjs";
|
import Landing from "@/components/landing"
|
||||||
import { redirect } from "next/navigation";
|
import { currentUser } from "@clerk/nextjs"
|
||||||
import Landing from "@/components/landing";
|
import { redirect } from "next/navigation"
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const user = await currentUser();
|
const user = await currentUser()
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
redirect("/dashboard");
|
redirect("/dashboard")
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Landing />;
|
return <Landing />
|
||||||
}
|
}
|
||||||
|
@ -3,16 +3,9 @@
|
|||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import Image from "next/image"
|
|
||||||
import { useState } from "react"
|
|
||||||
|
|
||||||
import { Button } from "../ui/button"
|
|
||||||
import { ChevronRight } from "lucide-react"
|
|
||||||
|
|
||||||
export default function AboutModal({
|
export default function AboutModal({
|
||||||
open,
|
open,
|
||||||
|
@ -1,24 +1,16 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import CustomButton from "@/components/ui/customButton"
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import CustomButton from "@/components/ui/customButton"
|
||||||
Code2,
|
|
||||||
FolderDot,
|
|
||||||
HelpCircle,
|
|
||||||
Plus,
|
|
||||||
Settings,
|
|
||||||
Users,
|
|
||||||
} from "lucide-react"
|
|
||||||
import { useEffect, useState } from "react"
|
|
||||||
import { Sandbox } from "@/lib/types"
|
import { Sandbox } from "@/lib/types"
|
||||||
|
import { Code2, FolderDot, HelpCircle, Plus, Users } from "lucide-react"
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
import AboutModal from "./about"
|
||||||
|
import NewProjectModal from "./newProject"
|
||||||
import DashboardProjects from "./projects"
|
import DashboardProjects from "./projects"
|
||||||
import DashboardSharedWithMe from "./shared"
|
import DashboardSharedWithMe from "./shared"
|
||||||
import NewProjectModal from "./newProject"
|
|
||||||
import Link from "next/link"
|
|
||||||
import { useRouter, useSearchParams } from "next/navigation"
|
|
||||||
import AboutModal from "./about"
|
|
||||||
import { toast } from "sonner"
|
|
||||||
|
|
||||||
type TScreen = "projects" | "shared" | "settings" | "search"
|
type TScreen = "projects" | "shared" | "settings" | "search"
|
||||||
|
|
||||||
@ -49,7 +41,8 @@ export default function Dashboard({
|
|||||||
const q = searchParams.get("q")
|
const q = searchParams.get("q")
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
useEffect(() => { // update the dashboard to show a new project
|
useEffect(() => {
|
||||||
|
// update the dashboard to show a new project
|
||||||
router.refresh()
|
router.refresh()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -102,7 +95,7 @@ export default function Dashboard({
|
|||||||
</Button> */}
|
</Button> */}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<a target="_blank" href="https://github.com/ishaan1013/sandbox">
|
<a target="_blank" href="https://github.com/jamesmurdza/sandbox">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="justify-start w-full font-normal text-muted-foreground"
|
className="justify-start w-full font-normal text-muted-foreground"
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
|
import Logo from "@/assets/logo.svg"
|
||||||
|
import { ThemeSwitcher } from "@/components/ui/theme-switcher"
|
||||||
|
import { User } from "@/lib/types"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import Logo from "@/assets/logo.svg"
|
|
||||||
import DashboardNavbarSearch from "./search"
|
|
||||||
import UserButton from "../../ui/userButton"
|
import UserButton from "../../ui/userButton"
|
||||||
import { User } from "@/lib/types"
|
import DashboardNavbarSearch from "./search"
|
||||||
|
|
||||||
export default function DashboardNavbar({ userData }: { userData: User }) {
|
export default function DashboardNavbar({ userData }: { userData: User }) {
|
||||||
return (
|
return (
|
||||||
@ -19,6 +20,7 @@ export default function DashboardNavbar({ userData }: { userData: User }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<DashboardNavbarSearch />
|
<DashboardNavbarSearch />
|
||||||
|
<ThemeSwitcher />
|
||||||
<UserButton userData={userData} />
|
<UserButton userData={userData} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { Input } from "../../ui/input";
|
import { Search } from "lucide-react"
|
||||||
import { Search } from "lucide-react";
|
import { useRouter } from "next/navigation"
|
||||||
import { useEffect, useState } from "react";
|
import { Input } from "../../ui/input"
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
|
|
||||||
export default function DashboardNavbarSearch() {
|
export default function DashboardNavbarSearch() {
|
||||||
// const [search, setSearch] = useState("");
|
// const [search, setSearch] = useState("");
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
|
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// const delayDebounceFn = setTimeout(() => {
|
// const delayDebounceFn = setTimeout(() => {
|
||||||
@ -29,14 +28,14 @@ export default function DashboardNavbarSearch() {
|
|||||||
// onChange={(e) => setSearch(e.target.value)}
|
// onChange={(e) => setSearch(e.target.value)}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (e.target.value === "") {
|
if (e.target.value === "") {
|
||||||
router.push(`/dashboard`);
|
router.push(`/dashboard`)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
router.push(`/dashboard?q=${e.target.value}`);
|
router.push(`/dashboard?q=${e.target.value}`)
|
||||||
}}
|
}}
|
||||||
placeholder="Search projects..."
|
placeholder="Search projects..."
|
||||||
className="pl-8"
|
className="pl-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -288,7 +288,7 @@ function SearchInput({
|
|||||||
<form {...{ onSubmit }} className="w-40 h-8 ">
|
<form {...{ onSubmit }} className="w-40 h-8 ">
|
||||||
<label
|
<label
|
||||||
htmlFor="template-search"
|
htmlFor="template-search"
|
||||||
className="flex gap-2 rounded-sm transition-colors bg-gray-100 dark:bg-[#2e2e2e] border border-[--s-color] [--s-color:hsl(var(--muted-foreground))] focus-within:[--s-color:#fff] h-full items-center px-2"
|
className="flex gap-2 rounded-sm transition-colors bg-gray-100 dark:bg-[#2e2e2e] border border-[--s-color] [--s-color:hsl(var(--muted-foreground))] focus-within:[--s-color:hsl(var(--muted-foreground),50%)] h-full items-center px-2"
|
||||||
>
|
>
|
||||||
<Search className="size-4 text-[--s-color] transition-colors" />
|
<Search className="size-4 text-[--s-color] transition-colors" />
|
||||||
<input
|
<input
|
||||||
@ -298,7 +298,7 @@ function SearchInput({
|
|||||||
placeholder="Search templates"
|
placeholder="Search templates"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onValueChange?.(e.target.value)}
|
onChange={(e) => onValueChange?.(e.target.value)}
|
||||||
className="bg-transparent placeholder:text-muted-foreground text-white w-full focus:outline-none text-xs"
|
className="bg-transparent placeholder:text-muted-foreground w-full focus:outline-none text-xs"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,30 +1,30 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { Sandbox } from "@/lib/types";
|
import { Sandbox } from "@/lib/types"
|
||||||
import { Ellipsis, Globe, Lock, Trash2 } from "lucide-react";
|
import { Ellipsis, Globe, Lock, Trash2 } from "lucide-react"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
|
||||||
export default function ProjectCardDropdown({
|
export default function ProjectCardDropdown({
|
||||||
sandbox,
|
sandbox,
|
||||||
onVisibilityChange,
|
onVisibilityChange,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: {
|
}: {
|
||||||
sandbox: Sandbox;
|
sandbox: Sandbox
|
||||||
onVisibilityChange: (sandbox: Sandbox) => void;
|
onVisibilityChange: (sandbox: Sandbox) => void
|
||||||
onDelete: (sandbox: Sandbox) => void;
|
onDelete: (sandbox: Sandbox) => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
e.stopPropagation();
|
e.stopPropagation()
|
||||||
}}
|
}}
|
||||||
className="h-6 w-6 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 rounded-sm outline-foreground"
|
className="h-6 w-6 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 rounded-sm outline-foreground"
|
||||||
>
|
>
|
||||||
@ -33,8 +33,8 @@ export default function ProjectCardDropdown({
|
|||||||
<DropdownMenuContent className="w-40">
|
<DropdownMenuContent className="w-40">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation()
|
||||||
onVisibilityChange(sandbox);
|
onVisibilityChange(sandbox)
|
||||||
}}
|
}}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
@ -52,8 +52,8 @@ export default function ProjectCardDropdown({
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation()
|
||||||
onDelete(sandbox);
|
onDelete(sandbox)
|
||||||
}}
|
}}
|
||||||
className="!text-destructive cursor-pointer"
|
className="!text-destructive cursor-pointer"
|
||||||
>
|
>
|
||||||
@ -62,5 +62,5 @@ export default function ProjectCardDropdown({
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { Card } from "@/components/ui/card"
|
||||||
|
import { projectTemplates } from "@/lib/data"
|
||||||
|
import { Sandbox } from "@/lib/types"
|
||||||
import { AnimatePresence, motion } from "framer-motion"
|
import { AnimatePresence, motion } from "framer-motion"
|
||||||
|
import { Clock, Globe, Lock } from "lucide-react"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import ProjectCardDropdown from "./dropdown"
|
import ProjectCardDropdown from "./dropdown"
|
||||||
import { Clock, Globe, Lock } from "lucide-react"
|
|
||||||
import { Sandbox } from "@/lib/types"
|
|
||||||
import { Card } from "@/components/ui/card"
|
|
||||||
import { useRouter } from "next/navigation"
|
|
||||||
import { projectTemplates } from "@/lib/data"
|
|
||||||
|
|
||||||
export default function ProjectCard({
|
export default function ProjectCard({
|
||||||
children,
|
children,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client"
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils"
|
||||||
import { Canvas, useFrame, useThree } from "@react-three/fiber";
|
import { Canvas, useFrame, useThree } from "@react-three/fiber"
|
||||||
import React, { useMemo, useRef } from "react";
|
import React, { useMemo, useRef } from "react"
|
||||||
import * as THREE from "three";
|
import * as THREE from "three"
|
||||||
|
|
||||||
export const CanvasRevealEffect = ({
|
export const CanvasRevealEffect = ({
|
||||||
animationSpeed = 0.4,
|
animationSpeed = 0.4,
|
||||||
@ -12,12 +12,12 @@ export const CanvasRevealEffect = ({
|
|||||||
dotSize,
|
dotSize,
|
||||||
showGradient = true,
|
showGradient = true,
|
||||||
}: {
|
}: {
|
||||||
animationSpeed?: number;
|
animationSpeed?: number
|
||||||
opacities?: number[];
|
opacities?: number[]
|
||||||
colors?: number[][];
|
colors?: number[][]
|
||||||
containerClassName?: string;
|
containerClassName?: string
|
||||||
dotSize?: number;
|
dotSize?: number
|
||||||
showGradient?: boolean;
|
showGradient?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className={cn("h-full relative bg-white w-full", containerClassName)}>
|
<div className={cn("h-full relative bg-white w-full", containerClassName)}>
|
||||||
@ -41,16 +41,16 @@ export const CanvasRevealEffect = ({
|
|||||||
<div className="absolute inset-0 bg-gradient-to-t from-background to-[100%]" />
|
<div className="absolute inset-0 bg-gradient-to-t from-background to-[100%]" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
interface DotMatrixProps {
|
interface DotMatrixProps {
|
||||||
colors?: number[][];
|
colors?: number[][]
|
||||||
opacities?: number[];
|
opacities?: number[]
|
||||||
totalSize?: number;
|
totalSize?: number
|
||||||
dotSize?: number;
|
dotSize?: number
|
||||||
shader?: string;
|
shader?: string
|
||||||
center?: ("x" | "y")[];
|
center?: ("x" | "y")[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const DotMatrix: React.FC<DotMatrixProps> = ({
|
const DotMatrix: React.FC<DotMatrixProps> = ({
|
||||||
@ -69,7 +69,7 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
|
|||||||
colors[0],
|
colors[0],
|
||||||
colors[0],
|
colors[0],
|
||||||
colors[0],
|
colors[0],
|
||||||
];
|
]
|
||||||
if (colors.length === 2) {
|
if (colors.length === 2) {
|
||||||
colorsArray = [
|
colorsArray = [
|
||||||
colors[0],
|
colors[0],
|
||||||
@ -78,7 +78,7 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
|
|||||||
colors[1],
|
colors[1],
|
||||||
colors[1],
|
colors[1],
|
||||||
colors[1],
|
colors[1],
|
||||||
];
|
]
|
||||||
} else if (colors.length === 3) {
|
} else if (colors.length === 3) {
|
||||||
colorsArray = [
|
colorsArray = [
|
||||||
colors[0],
|
colors[0],
|
||||||
@ -87,7 +87,7 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
|
|||||||
colors[1],
|
colors[1],
|
||||||
colors[2],
|
colors[2],
|
||||||
colors[2],
|
colors[2],
|
||||||
];
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -111,8 +111,8 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
|
|||||||
value: dotSize,
|
value: dotSize,
|
||||||
type: "uniform1f",
|
type: "uniform1f",
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
}, [colors, opacities, totalSize, dotSize]);
|
}, [colors, opacities, totalSize, dotSize])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shader
|
<Shader
|
||||||
@ -168,87 +168,87 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
|
|||||||
uniforms={uniforms}
|
uniforms={uniforms}
|
||||||
maxFps={60}
|
maxFps={60}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
type Uniforms = {
|
type Uniforms = {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
value: number[] | number[][] | number;
|
value: number[] | number[][] | number
|
||||||
type: string;
|
type: string
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
const ShaderMaterial = ({
|
const ShaderMaterial = ({
|
||||||
source,
|
source,
|
||||||
uniforms,
|
uniforms,
|
||||||
maxFps = 60,
|
maxFps = 60,
|
||||||
}: {
|
}: {
|
||||||
source: string;
|
source: string
|
||||||
hovered?: boolean;
|
hovered?: boolean
|
||||||
maxFps?: number;
|
maxFps?: number
|
||||||
uniforms: Uniforms;
|
uniforms: Uniforms
|
||||||
}) => {
|
}) => {
|
||||||
const { size } = useThree();
|
const { size } = useThree()
|
||||||
const ref = useRef<THREE.Mesh>();
|
const ref = useRef<THREE.Mesh>()
|
||||||
let lastFrameTime = 0;
|
let lastFrameTime = 0
|
||||||
|
|
||||||
useFrame(({ clock }) => {
|
useFrame(({ clock }) => {
|
||||||
if (!ref.current) return;
|
if (!ref.current) return
|
||||||
const timestamp = clock.getElapsedTime();
|
const timestamp = clock.getElapsedTime()
|
||||||
if (timestamp - lastFrameTime < 1 / maxFps) {
|
if (timestamp - lastFrameTime < 1 / maxFps) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
lastFrameTime = timestamp;
|
lastFrameTime = timestamp
|
||||||
|
|
||||||
const material: any = ref.current.material;
|
const material: any = ref.current.material
|
||||||
const timeLocation = material.uniforms.u_time;
|
const timeLocation = material.uniforms.u_time
|
||||||
timeLocation.value = timestamp;
|
timeLocation.value = timestamp
|
||||||
});
|
})
|
||||||
|
|
||||||
const getUniforms = () => {
|
const getUniforms = () => {
|
||||||
const preparedUniforms: any = {};
|
const preparedUniforms: any = {}
|
||||||
|
|
||||||
for (const uniformName in uniforms) {
|
for (const uniformName in uniforms) {
|
||||||
const uniform: any = uniforms[uniformName];
|
const uniform: any = uniforms[uniformName]
|
||||||
|
|
||||||
switch (uniform.type) {
|
switch (uniform.type) {
|
||||||
case "uniform1f":
|
case "uniform1f":
|
||||||
preparedUniforms[uniformName] = { value: uniform.value, type: "1f" };
|
preparedUniforms[uniformName] = { value: uniform.value, type: "1f" }
|
||||||
break;
|
break
|
||||||
case "uniform3f":
|
case "uniform3f":
|
||||||
preparedUniforms[uniformName] = {
|
preparedUniforms[uniformName] = {
|
||||||
value: new THREE.Vector3().fromArray(uniform.value),
|
value: new THREE.Vector3().fromArray(uniform.value),
|
||||||
type: "3f",
|
type: "3f",
|
||||||
};
|
}
|
||||||
break;
|
break
|
||||||
case "uniform1fv":
|
case "uniform1fv":
|
||||||
preparedUniforms[uniformName] = { value: uniform.value, type: "1fv" };
|
preparedUniforms[uniformName] = { value: uniform.value, type: "1fv" }
|
||||||
break;
|
break
|
||||||
case "uniform3fv":
|
case "uniform3fv":
|
||||||
preparedUniforms[uniformName] = {
|
preparedUniforms[uniformName] = {
|
||||||
value: uniform.value.map((v: number[]) =>
|
value: uniform.value.map((v: number[]) =>
|
||||||
new THREE.Vector3().fromArray(v)
|
new THREE.Vector3().fromArray(v)
|
||||||
),
|
),
|
||||||
type: "3fv",
|
type: "3fv",
|
||||||
};
|
}
|
||||||
break;
|
break
|
||||||
case "uniform2f":
|
case "uniform2f":
|
||||||
preparedUniforms[uniformName] = {
|
preparedUniforms[uniformName] = {
|
||||||
value: new THREE.Vector2().fromArray(uniform.value),
|
value: new THREE.Vector2().fromArray(uniform.value),
|
||||||
type: "2f",
|
type: "2f",
|
||||||
};
|
}
|
||||||
break;
|
break
|
||||||
default:
|
default:
|
||||||
console.error(`Invalid uniform type for '${uniformName}'.`);
|
console.error(`Invalid uniform type for '${uniformName}'.`)
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
preparedUniforms["u_time"] = { value: 0, type: "1f" };
|
preparedUniforms["u_time"] = { value: 0, type: "1f" }
|
||||||
preparedUniforms["u_resolution"] = {
|
preparedUniforms["u_resolution"] = {
|
||||||
value: new THREE.Vector2(size.width * 2, size.height * 2),
|
value: new THREE.Vector2(size.width * 2, size.height * 2),
|
||||||
}; // Initialize u_resolution
|
} // Initialize u_resolution
|
||||||
return preparedUniforms;
|
return preparedUniforms
|
||||||
};
|
}
|
||||||
|
|
||||||
// Shader material
|
// Shader material
|
||||||
const material = useMemo(() => {
|
const material = useMemo(() => {
|
||||||
@ -272,33 +272,33 @@ const ShaderMaterial = ({
|
|||||||
blending: THREE.CustomBlending,
|
blending: THREE.CustomBlending,
|
||||||
blendSrc: THREE.SrcAlphaFactor,
|
blendSrc: THREE.SrcAlphaFactor,
|
||||||
blendDst: THREE.OneFactor,
|
blendDst: THREE.OneFactor,
|
||||||
});
|
})
|
||||||
|
|
||||||
return materialObject;
|
return materialObject
|
||||||
}, [size.width, size.height, source]);
|
}, [size.width, size.height, source])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh ref={ref as any}>
|
<mesh ref={ref as any}>
|
||||||
<planeGeometry args={[2, 2]} />
|
<planeGeometry args={[2, 2]} />
|
||||||
<primitive object={material} attach="material" />
|
<primitive object={material} attach="material" />
|
||||||
</mesh>
|
</mesh>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const Shader: React.FC<ShaderProps> = ({ source, uniforms, maxFps = 60 }) => {
|
const Shader: React.FC<ShaderProps> = ({ source, uniforms, maxFps = 60 }) => {
|
||||||
return (
|
return (
|
||||||
<Canvas className="absolute inset-0 h-full w-full">
|
<Canvas className="absolute inset-0 h-full w-full">
|
||||||
<ShaderMaterial source={source} uniforms={uniforms} maxFps={maxFps} />
|
<ShaderMaterial source={source} uniforms={uniforms} maxFps={maxFps} />
|
||||||
</Canvas>
|
</Canvas>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
interface ShaderProps {
|
interface ShaderProps {
|
||||||
source: string;
|
source: string
|
||||||
uniforms: {
|
uniforms: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
value: number[] | number[][] | number;
|
value: number[] | number[][] | number
|
||||||
type: string;
|
type: string
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
maxFps?: number;
|
maxFps?: number
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,12 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { Sandbox } from "@/lib/types";
|
import { deleteSandbox, updateSandbox } from "@/lib/actions"
|
||||||
import ProjectCard from "./projectCard";
|
import { Sandbox } from "@/lib/types"
|
||||||
import Image from "next/image";
|
import Link from "next/link"
|
||||||
import ProjectCardDropdown from "./projectCard/dropdown";
|
import { useEffect, useState } from "react"
|
||||||
import { Clock, Globe, Lock } from "lucide-react";
|
import { toast } from "sonner"
|
||||||
import Link from "next/link";
|
import ProjectCard from "./projectCard"
|
||||||
import { Card } from "../ui/card";
|
import { CanvasRevealEffect } from "./projectCard/revealEffect"
|
||||||
import { deleteSandbox, updateSandbox } from "@/lib/actions";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { CanvasRevealEffect } from "./projectCard/revealEffect";
|
|
||||||
|
|
||||||
const colors: { [key: string]: number[][] } = {
|
const colors: { [key: string]: number[][] } = {
|
||||||
react: [
|
react: [
|
||||||
@ -21,38 +17,37 @@ const colors: { [key: string]: number[][] } = {
|
|||||||
[86, 184, 72],
|
[86, 184, 72],
|
||||||
[59, 112, 52],
|
[59, 112, 52],
|
||||||
],
|
],
|
||||||
};
|
}
|
||||||
|
|
||||||
export default function DashboardProjects({
|
export default function DashboardProjects({
|
||||||
sandboxes,
|
sandboxes,
|
||||||
q,
|
q,
|
||||||
}: {
|
}: {
|
||||||
sandboxes: Sandbox[];
|
sandboxes: Sandbox[]
|
||||||
q: string | null;
|
q: string | null
|
||||||
}) {
|
}) {
|
||||||
const [deletingId, setDeletingId] = useState<string>("");
|
const [deletingId, setDeletingId] = useState<string>("")
|
||||||
|
|
||||||
const onDelete = async (sandbox: Sandbox) => {
|
const onDelete = async (sandbox: Sandbox) => {
|
||||||
setDeletingId(sandbox.id);
|
setDeletingId(sandbox.id)
|
||||||
toast(`Project ${sandbox.name} deleted.`);
|
toast(`Project ${sandbox.name} deleted.`)
|
||||||
await deleteSandbox(sandbox.id);
|
await deleteSandbox(sandbox.id)
|
||||||
};
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (deletingId) {
|
if (deletingId) {
|
||||||
setDeletingId("");
|
setDeletingId("")
|
||||||
}
|
}
|
||||||
}, [sandboxes]);
|
}, [sandboxes])
|
||||||
|
|
||||||
const onVisibilityChange = async (sandbox: Sandbox) => {
|
const onVisibilityChange = async (sandbox: Sandbox) => {
|
||||||
const newVisibility =
|
const newVisibility = sandbox.visibility === "public" ? "private" : "public"
|
||||||
sandbox.visibility === "public" ? "private" : "public";
|
toast(`Project ${sandbox.name} is now ${newVisibility}.`)
|
||||||
toast(`Project ${sandbox.name} is now ${newVisibility}.`);
|
|
||||||
await updateSandbox({
|
await updateSandbox({
|
||||||
id: sandbox.id,
|
id: sandbox.id,
|
||||||
visibility: newVisibility,
|
visibility: newVisibility,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grow p-4 flex flex-col">
|
<div className="grow p-4 flex flex-col">
|
||||||
@ -65,7 +60,7 @@ export default function DashboardProjects({
|
|||||||
{sandboxes.map((sandbox) => {
|
{sandboxes.map((sandbox) => {
|
||||||
if (q && q.length > 0) {
|
if (q && q.length > 0) {
|
||||||
if (!sandbox.name.toLowerCase().includes(q.toLowerCase())) {
|
if (!sandbox.name.toLowerCase().includes(q.toLowerCase())) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@ -93,7 +88,7 @@ export default function DashboardProjects({
|
|||||||
<div className="absolute inset-0 [mask-image:radial-gradient(400px_at_center,white,transparent)] bg-background/75" />
|
<div className="absolute inset-0 [mask-image:radial-gradient(400px_at_center,white,transparent)] bg-background/75" />
|
||||||
</ProjectCard>
|
</ProjectCard>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -103,5 +98,5 @@ export default function DashboardProjects({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,29 +1,27 @@
|
|||||||
import { Sandbox } from "@/lib/types";
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCaption,
|
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table"
|
||||||
import Image from "next/image";
|
import { ChevronRight } from "lucide-react"
|
||||||
import Button from "../ui/customButton";
|
import Image from "next/image"
|
||||||
import { ChevronRight } from "lucide-react";
|
import Link from "next/link"
|
||||||
import Avatar from "../ui/avatar";
|
import Avatar from "../ui/avatar"
|
||||||
import Link from "next/link";
|
import Button from "../ui/customButton"
|
||||||
|
|
||||||
export default function DashboardSharedWithMe({
|
export default function DashboardSharedWithMe({
|
||||||
shared,
|
shared,
|
||||||
}: {
|
}: {
|
||||||
shared: {
|
shared: {
|
||||||
id: string;
|
id: string
|
||||||
name: string;
|
name: string
|
||||||
type: "react" | "node";
|
type: "react" | "node"
|
||||||
author: string;
|
author: string
|
||||||
sharedOn: Date;
|
sharedOn: Date
|
||||||
}[];
|
}[]
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="grow p-4 flex flex-col">
|
<div className="grow p-4 flex flex-col">
|
||||||
@ -86,5 +84,5 @@ export default function DashboardSharedWithMe({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,36 +1,51 @@
|
|||||||
import React from 'react';
|
import { Send, StopCircle } from "lucide-react"
|
||||||
import { Button } from '../../ui/button';
|
import { Button } from "../../ui/button"
|
||||||
import { Send, StopCircle } from 'lucide-react';
|
|
||||||
|
|
||||||
interface ChatInputProps {
|
interface ChatInputProps {
|
||||||
input: string;
|
input: string
|
||||||
setInput: (input: string) => void;
|
setInput: (input: string) => void
|
||||||
isGenerating: boolean;
|
isGenerating: boolean
|
||||||
handleSend: () => void;
|
handleSend: () => void
|
||||||
handleStopGeneration: () => void;
|
handleStopGeneration: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChatInput({ input, setInput, isGenerating, handleSend, handleStopGeneration }: ChatInputProps) {
|
export default function ChatInput({
|
||||||
|
input,
|
||||||
|
setInput,
|
||||||
|
isGenerating,
|
||||||
|
handleSend,
|
||||||
|
handleStopGeneration,
|
||||||
|
}: ChatInputProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex space-x-2 min-w-0">
|
<div className="flex space-x-2 min-w-0">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
onKeyPress={(e) => e.key === 'Enter' && !isGenerating && handleSend()}
|
onKeyPress={(e) => e.key === "Enter" && !isGenerating && handleSend()}
|
||||||
className="flex-grow p-2 border rounded-lg min-w-0 bg-input"
|
className="flex-grow p-2 border rounded-lg min-w-0 bg-input"
|
||||||
placeholder="Type your message..."
|
placeholder="Type your message..."
|
||||||
disabled={isGenerating}
|
disabled={isGenerating}
|
||||||
/>
|
/>
|
||||||
{isGenerating ? (
|
{isGenerating ? (
|
||||||
<Button onClick={handleStopGeneration} variant="destructive" size="icon" className="h-10 w-10">
|
<Button
|
||||||
|
onClick={handleStopGeneration}
|
||||||
|
variant="destructive"
|
||||||
|
size="icon"
|
||||||
|
className="h-10 w-10"
|
||||||
|
>
|
||||||
<StopCircle className="w-4 h-4" />
|
<StopCircle className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button onClick={handleSend} disabled={isGenerating} size="icon" className="h-10 w-10">
|
<Button
|
||||||
|
onClick={handleSend}
|
||||||
|
disabled={isGenerating}
|
||||||
|
size="icon"
|
||||||
|
className="h-10 w-10"
|
||||||
|
>
|
||||||
<Send className="w-4 h-4" />
|
<Send className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,31 @@
|
|||||||
import React, { useState } from 'react';
|
import { Check, ChevronDown, ChevronUp, Copy, CornerUpLeft } from "lucide-react"
|
||||||
import { Button } from '../../ui/button';
|
import React, { useState } from "react"
|
||||||
import { ChevronUp, ChevronDown, Copy, Check, CornerUpLeft } from 'lucide-react';
|
import ReactMarkdown from "react-markdown"
|
||||||
import ReactMarkdown from 'react-markdown';
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
||||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"
|
||||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
import remarkGfm from "remark-gfm"
|
||||||
import remarkGfm from 'remark-gfm';
|
import { Button } from "../../ui/button"
|
||||||
import { copyToClipboard, stringifyContent } from './lib/chatUtils';
|
import { copyToClipboard, stringifyContent } from "./lib/chatUtils"
|
||||||
|
|
||||||
interface MessageProps {
|
interface MessageProps {
|
||||||
message: {
|
message: {
|
||||||
role: 'user' | 'assistant';
|
role: "user" | "assistant"
|
||||||
content: string;
|
content: string
|
||||||
context?: string;
|
context?: string
|
||||||
};
|
}
|
||||||
setContext: (context: string | null) => void;
|
setContext: (context: string | null) => void
|
||||||
setIsContextExpanded: (isExpanded: boolean) => void;
|
setIsContextExpanded: (isExpanded: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChatMessage({ message, setContext, setIsContextExpanded }: MessageProps) {
|
export default function ChatMessage({
|
||||||
const [expandedMessageIndex, setExpandedMessageIndex] = useState<number | null>(null);
|
message,
|
||||||
const [copiedText, setCopiedText] = useState<string | null>(null);
|
setContext,
|
||||||
|
setIsContextExpanded,
|
||||||
|
}: MessageProps) {
|
||||||
|
const [expandedMessageIndex, setExpandedMessageIndex] = useState<
|
||||||
|
number | null
|
||||||
|
>(null)
|
||||||
|
const [copiedText, setCopiedText] = useState<string | null>(null)
|
||||||
|
|
||||||
const renderCopyButton = (text: any) => (
|
const renderCopyButton = (text: any) => (
|
||||||
<Button
|
<Button
|
||||||
@ -34,17 +40,17 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
|
|||||||
<Copy className="w-4 h-4" />
|
<Copy className="w-4 h-4" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
)
|
||||||
|
|
||||||
const askAboutCode = (code: any) => {
|
const askAboutCode = (code: any) => {
|
||||||
const contextString = stringifyContent(code);
|
const contextString = stringifyContent(code)
|
||||||
setContext(`Regarding this code:\n${contextString}`);
|
setContext(`Regarding this code:\n${contextString}`)
|
||||||
setIsContextExpanded(false);
|
setIsContextExpanded(false)
|
||||||
};
|
}
|
||||||
|
|
||||||
const renderMarkdownElement = (props: any) => {
|
const renderMarkdownElement = (props: any) => {
|
||||||
const { node, children } = props;
|
const { node, children } = props
|
||||||
const content = stringifyContent(children);
|
const content = stringifyContent(children)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
@ -59,22 +65,30 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
|
|||||||
<CornerUpLeft className="w-4 h-4" />
|
<CornerUpLeft className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{React.createElement(node.tagName, {
|
{React.createElement(
|
||||||
|
node.tagName,
|
||||||
|
{
|
||||||
...props,
|
...props,
|
||||||
className: `${props.className || ''} hover:bg-transparent rounded p-1 transition-colors`
|
className: `${
|
||||||
}, children)}
|
props.className || ""
|
||||||
|
} hover:bg-transparent rounded p-1 transition-colors`,
|
||||||
|
},
|
||||||
|
children
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-left relative">
|
<div className="text-left relative">
|
||||||
<div className={`relative p-2 rounded-lg ${
|
<div
|
||||||
message.role === 'user'
|
className={`relative p-2 rounded-lg ${
|
||||||
? 'bg-[#262626] text-white'
|
message.role === "user"
|
||||||
: 'bg-transparent text-white'
|
? "bg-[#262626] text-white"
|
||||||
} max-w-full`}>
|
: "bg-transparent text-white"
|
||||||
{message.role === 'user' && (
|
} max-w-full`}
|
||||||
|
>
|
||||||
|
{message.role === "user" && (
|
||||||
<div className="absolute top-0 right-0 flex opacity-0 group-hover:opacity-30 transition-opacity">
|
<div className="absolute top-0 right-0 flex opacity-0 group-hover:opacity-30 transition-opacity">
|
||||||
{renderCopyButton(message.content)}
|
{renderCopyButton(message.content)}
|
||||||
<Button
|
<Button
|
||||||
@ -91,11 +105,11 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
|
|||||||
<div className="mb-2 bg-input rounded-lg">
|
<div className="mb-2 bg-input rounded-lg">
|
||||||
<div
|
<div
|
||||||
className="flex justify-between items-center cursor-pointer"
|
className="flex justify-between items-center cursor-pointer"
|
||||||
onClick={() => setExpandedMessageIndex(expandedMessageIndex === 0 ? null : 0)}
|
onClick={() =>
|
||||||
|
setExpandedMessageIndex(expandedMessageIndex === 0 ? null : 0)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span className="text-sm text-gray-300">
|
<span className="text-sm text-gray-300">Context</span>
|
||||||
Context
|
|
||||||
</span>
|
|
||||||
{expandedMessageIndex === 0 ? (
|
{expandedMessageIndex === 0 ? (
|
||||||
<ChevronUp size={16} />
|
<ChevronUp size={16} />
|
||||||
) : (
|
) : (
|
||||||
@ -105,41 +119,46 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
|
|||||||
{expandedMessageIndex === 0 && (
|
{expandedMessageIndex === 0 && (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute top-0 right-0 flex p-1">
|
<div className="absolute top-0 right-0 flex p-1">
|
||||||
{renderCopyButton(message.context.replace(/^Regarding this code:\n/, ''))}
|
{renderCopyButton(
|
||||||
|
message.context.replace(/^Regarding this code:\n/, "")
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
const code = message.context.replace(/^Regarding this code:\n/, '');
|
const code = message.context.replace(
|
||||||
const match = /language-(\w+)/.exec(code);
|
/^Regarding this code:\n/,
|
||||||
const language = match ? match[1] : 'typescript';
|
""
|
||||||
|
)
|
||||||
|
const match = /language-(\w+)/.exec(code)
|
||||||
|
const language = match ? match[1] : "typescript"
|
||||||
return (
|
return (
|
||||||
<div className="pt-6">
|
<div className="pt-6">
|
||||||
<textarea
|
<textarea
|
||||||
value={code}
|
value={code}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const updatedContext = `Regarding this code:\n${e.target.value}`;
|
const updatedContext = `Regarding this code:\n${e.target.value}`
|
||||||
setContext(updatedContext);
|
setContext(updatedContext)
|
||||||
}}
|
}}
|
||||||
className="w-full p-2 bg-[#1e1e1e] text-white font-mono text-sm rounded"
|
className="w-full p-2 bg-[#1e1e1e] text-white font-mono text-sm rounded"
|
||||||
rows={code.split('\n').length}
|
rows={code.split("\n").length}
|
||||||
style={{
|
style={{
|
||||||
resize: 'vertical',
|
resize: "vertical",
|
||||||
minHeight: '100px',
|
minHeight: "100px",
|
||||||
maxHeight: '400px',
|
maxHeight: "400px",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{message.role === 'assistant' ? (
|
{message.role === "assistant" ? (
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm]}
|
||||||
components={{
|
components={{
|
||||||
code({node, className, children, ...props}) {
|
code({ node, className, children, ...props }) {
|
||||||
const match = /language-(\w+)/.exec(className || '');
|
const match = /language-(\w+)/.exec(className || "")
|
||||||
return match ? (
|
return match ? (
|
||||||
<div className="relative border border-input rounded-md my-4">
|
<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="absolute top-0 left-0 px-2 py-1 text-xs font-semibold text-gray-200 bg-#1e1e1e rounded-tl">
|
||||||
@ -163,8 +182,8 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
|
|||||||
PreTag="div"
|
PreTag="div"
|
||||||
customStyle={{
|
customStyle={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: '0.5rem',
|
padding: "0.5rem",
|
||||||
fontSize: '0.875rem',
|
fontSize: "0.875rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{stringifyContent(children)}
|
{stringifyContent(children)}
|
||||||
@ -175,7 +194,7 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
|
|||||||
<code className={className} {...props}>
|
<code className={className} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</code>
|
</code>
|
||||||
);
|
)
|
||||||
},
|
},
|
||||||
p: renderMarkdownElement,
|
p: renderMarkdownElement,
|
||||||
h1: renderMarkdownElement,
|
h1: renderMarkdownElement,
|
||||||
@ -184,18 +203,24 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
|
|||||||
h4: renderMarkdownElement,
|
h4: renderMarkdownElement,
|
||||||
h5: renderMarkdownElement,
|
h5: renderMarkdownElement,
|
||||||
h6: renderMarkdownElement,
|
h6: renderMarkdownElement,
|
||||||
ul: (props) => <ul className="list-disc pl-6 mb-4 space-y-2">{props.children}</ul>,
|
ul: (props) => (
|
||||||
ol: (props) => <ol className="list-decimal pl-6 mb-4 space-y-2">{props.children}</ol>,
|
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||||
|
{props.children}
|
||||||
|
</ul>
|
||||||
|
),
|
||||||
|
ol: (props) => (
|
||||||
|
<ol className="list-decimal pl-6 mb-4 space-y-2">
|
||||||
|
{props.children}
|
||||||
|
</ol>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{message.content}
|
{message.content}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
) : (
|
) : (
|
||||||
<div className="whitespace-pre-wrap group">
|
<div className="whitespace-pre-wrap group">{message.content}</div>
|
||||||
{message.content}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
import React from 'react';
|
import { ChevronDown, ChevronUp, X } from "lucide-react"
|
||||||
import { ChevronUp, ChevronDown, X } from 'lucide-react';
|
|
||||||
|
|
||||||
interface ContextDisplayProps {
|
interface ContextDisplayProps {
|
||||||
context: string | null;
|
context: string | null
|
||||||
isContextExpanded: boolean;
|
isContextExpanded: boolean
|
||||||
setIsContextExpanded: (isExpanded: boolean) => void;
|
setIsContextExpanded: (isExpanded: boolean) => void
|
||||||
setContext: (context: string | null) => void;
|
setContext: (context: string | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ContextDisplay({ context, isContextExpanded, setIsContextExpanded, setContext }: ContextDisplayProps) {
|
export default function ContextDisplay({
|
||||||
if (!context) return null;
|
context,
|
||||||
|
isContextExpanded,
|
||||||
|
setIsContextExpanded,
|
||||||
|
setContext,
|
||||||
|
}: ContextDisplayProps) {
|
||||||
|
if (!context) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-2 bg-input p-2 rounded-lg">
|
<div className="mb-2 bg-input p-2 rounded-lg">
|
||||||
@ -18,15 +22,21 @@ export default function ContextDisplay({ context, isContextExpanded, setIsContex
|
|||||||
className="flex-grow cursor-pointer"
|
className="flex-grow cursor-pointer"
|
||||||
onClick={() => setIsContextExpanded(!isContextExpanded)}
|
onClick={() => setIsContextExpanded(!isContextExpanded)}
|
||||||
>
|
>
|
||||||
<span className="text-sm text-gray-300">
|
<span className="text-sm text-gray-300">Context</span>
|
||||||
Context
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{isContextExpanded ? (
|
{isContextExpanded ? (
|
||||||
<ChevronUp size={16} className="cursor-pointer" onClick={() => setIsContextExpanded(false)} />
|
<ChevronUp
|
||||||
|
size={16}
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => setIsContextExpanded(false)}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ChevronDown size={16} className="cursor-pointer" onClick={() => setIsContextExpanded(true)} />
|
<ChevronDown
|
||||||
|
size={16}
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => setIsContextExpanded(true)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<X
|
<X
|
||||||
size={16}
|
size={16}
|
||||||
@ -37,12 +47,14 @@ export default function ContextDisplay({ context, isContextExpanded, setIsContex
|
|||||||
</div>
|
</div>
|
||||||
{isContextExpanded && (
|
{isContextExpanded && (
|
||||||
<textarea
|
<textarea
|
||||||
value={context.replace(/^Regarding this code:\n/, '')}
|
value={context.replace(/^Regarding this code:\n/, "")}
|
||||||
onChange={(e) => setContext(`Regarding this code:\n${e.target.value}`)}
|
onChange={(e) =>
|
||||||
|
setContext(`Regarding this code:\n${e.target.value}`)
|
||||||
|
}
|
||||||
className="w-full mt-2 p-2 bg-#1e1e1e text-white rounded"
|
className="w-full mt-2 p-2 bg-#1e1e1e text-white rounded"
|
||||||
rows={5}
|
rows={5}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,48 +1,72 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import { X } from "lucide-react"
|
||||||
import LoadingDots from '../../ui/LoadingDots';
|
import { useEffect, useRef, useState } from "react"
|
||||||
import ChatMessage from './ChatMessage';
|
import LoadingDots from "../../ui/LoadingDots"
|
||||||
import ChatInput from './ChatInput';
|
import ChatInput from "./ChatInput"
|
||||||
import ContextDisplay from './ContextDisplay';
|
import ChatMessage from "./ChatMessage"
|
||||||
import { handleSend, handleStopGeneration } from './lib/chatUtils';
|
import ContextDisplay from "./ContextDisplay"
|
||||||
|
import { handleSend, handleStopGeneration } from "./lib/chatUtils"
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
role: 'user' | 'assistant';
|
role: "user" | "assistant"
|
||||||
content: string;
|
content: string
|
||||||
context?: string;
|
context?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AIChat({ activeFileContent, activeFileName }: { activeFileContent: string, activeFileName: string }) {
|
export default function AIChat({
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
activeFileContent,
|
||||||
const [input, setInput] = useState('');
|
activeFileName,
|
||||||
const [isGenerating, setIsGenerating] = useState(false);
|
onClose,
|
||||||
const chatContainerRef = useRef<HTMLDivElement>(null);
|
}: {
|
||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
activeFileContent: string
|
||||||
const [context, setContext] = useState<string | null>(null);
|
activeFileName: string
|
||||||
const [isContextExpanded, setIsContextExpanded] = useState(false);
|
onClose: () => void
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
}) {
|
||||||
|
const [messages, setMessages] = useState<Message[]>([])
|
||||||
|
const [input, setInput] = useState("")
|
||||||
|
const [isGenerating, setIsGenerating] = useState(false)
|
||||||
|
const chatContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const abortControllerRef = useRef<AbortController | null>(null)
|
||||||
|
const [context, setContext] = useState<string | null>(null)
|
||||||
|
const [isContextExpanded, setIsContextExpanded] = useState(false)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToBottom();
|
scrollToBottom()
|
||||||
}, [messages]);
|
}, [messages])
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
if (chatContainerRef.current) {
|
if (chatContainerRef.current) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
chatContainerRef.current?.scrollTo({
|
chatContainerRef.current?.scrollTo({
|
||||||
top: chatContainerRef.current.scrollHeight,
|
top: chatContainerRef.current.scrollHeight,
|
||||||
behavior: 'smooth'
|
behavior: "smooth",
|
||||||
});
|
})
|
||||||
}, 100);
|
}, 100)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-screen w-full">
|
<div className="flex flex-col h-screen w-full">
|
||||||
<div className="flex justify-between items-center p-2 border-b">
|
<div className="flex justify-between items-center p-2 border-b">
|
||||||
<span className="text-muted-foreground/50 font-medium">CHAT</span>
|
<span className="text-muted-foreground/50 font-medium">CHAT</span>
|
||||||
<span className="text-muted-foreground/50 font-medium truncate max-w-[50%]" title={activeFileName}>{activeFileName}</span>
|
<div className="flex items-center h-full">
|
||||||
|
<span className="text-muted-foreground/50 font-medium">
|
||||||
|
{activeFileName}
|
||||||
|
</span>
|
||||||
|
<div className="mx-2 h-full w-px bg-muted-foreground/20"></div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-muted-foreground/50 hover:text-muted-foreground focus:outline-none"
|
||||||
|
aria-label="Close AI Chat"
|
||||||
|
>
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div ref={chatContainerRef} className="flex-grow overflow-y-auto p-4 space-y-4">
|
</div>
|
||||||
|
<div
|
||||||
|
ref={chatContainerRef}
|
||||||
|
className="flex-grow overflow-y-auto p-4 space-y-4"
|
||||||
|
>
|
||||||
{messages.map((message, messageIndex) => (
|
{messages.map((message, messageIndex) => (
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
key={messageIndex}
|
key={messageIndex}
|
||||||
@ -64,10 +88,23 @@ export default function AIChat({ activeFileContent, activeFileName }: { activeFi
|
|||||||
input={input}
|
input={input}
|
||||||
setInput={setInput}
|
setInput={setInput}
|
||||||
isGenerating={isGenerating}
|
isGenerating={isGenerating}
|
||||||
handleSend={() => handleSend(input, context, messages, setMessages, setInput, setIsContextExpanded, setIsGenerating, setIsLoading, abortControllerRef, activeFileContent)}
|
handleSend={() =>
|
||||||
|
handleSend(
|
||||||
|
input,
|
||||||
|
context,
|
||||||
|
messages,
|
||||||
|
setMessages,
|
||||||
|
setInput,
|
||||||
|
setIsContextExpanded,
|
||||||
|
setIsGenerating,
|
||||||
|
setIsLoading,
|
||||||
|
abortControllerRef,
|
||||||
|
activeFileContent
|
||||||
|
)
|
||||||
|
}
|
||||||
handleStopGeneration={() => handleStopGeneration(abortControllerRef)}
|
handleStopGeneration={() => handleStopGeneration(abortControllerRef)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,58 +1,68 @@
|
|||||||
import React from 'react';
|
import React from "react"
|
||||||
|
|
||||||
export const stringifyContent = (content: any, seen = new WeakSet()): string => {
|
export const stringifyContent = (
|
||||||
if (typeof content === 'string') {
|
content: any,
|
||||||
return content;
|
seen = new WeakSet()
|
||||||
|
): string => {
|
||||||
|
if (typeof content === "string") {
|
||||||
|
return content
|
||||||
}
|
}
|
||||||
if (content === null) {
|
if (content === null) {
|
||||||
return 'null';
|
return "null"
|
||||||
}
|
}
|
||||||
if (content === undefined) {
|
if (content === undefined) {
|
||||||
return 'undefined';
|
return "undefined"
|
||||||
}
|
}
|
||||||
if (typeof content === 'number' || typeof content === 'boolean') {
|
if (typeof content === "number" || typeof content === "boolean") {
|
||||||
return content.toString();
|
return content.toString()
|
||||||
}
|
}
|
||||||
if (typeof content === 'function') {
|
if (typeof content === "function") {
|
||||||
return content.toString();
|
return content.toString()
|
||||||
}
|
}
|
||||||
if (typeof content === 'symbol') {
|
if (typeof content === "symbol") {
|
||||||
return content.toString();
|
return content.toString()
|
||||||
}
|
}
|
||||||
if (typeof content === 'bigint') {
|
if (typeof content === "bigint") {
|
||||||
return content.toString() + 'n';
|
return content.toString() + "n"
|
||||||
}
|
}
|
||||||
if (React.isValidElement(content)) {
|
if (React.isValidElement(content)) {
|
||||||
return React.Children.toArray((content as React.ReactElement).props.children)
|
return React.Children.toArray(
|
||||||
.map(child => stringifyContent(child, seen))
|
(content as React.ReactElement).props.children
|
||||||
.join('');
|
)
|
||||||
|
.map((child) => stringifyContent(child, seen))
|
||||||
|
.join("")
|
||||||
}
|
}
|
||||||
if (Array.isArray(content)) {
|
if (Array.isArray(content)) {
|
||||||
return '[' + content.map(item => stringifyContent(item, seen)).join(', ') + ']';
|
return (
|
||||||
|
"[" + content.map((item) => stringifyContent(item, seen)).join(", ") + "]"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (typeof content === 'object') {
|
if (typeof content === "object") {
|
||||||
if (seen.has(content)) {
|
if (seen.has(content)) {
|
||||||
return '[Circular]';
|
return "[Circular]"
|
||||||
}
|
}
|
||||||
seen.add(content);
|
seen.add(content)
|
||||||
try {
|
try {
|
||||||
const pairs = Object.entries(content).map(
|
const pairs = Object.entries(content).map(
|
||||||
([key, value]) => `${key}: ${stringifyContent(value, seen)}`
|
([key, value]) => `${key}: ${stringifyContent(value, seen)}`
|
||||||
);
|
)
|
||||||
return '{' + pairs.join(', ') + '}';
|
return "{" + pairs.join(", ") + "}"
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Object.prototype.toString.call(content);
|
return Object.prototype.toString.call(content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return String(content);
|
return String(content)
|
||||||
};
|
}
|
||||||
|
|
||||||
export const copyToClipboard = (text: string, setCopiedText: (text: string | null) => void) => {
|
export const copyToClipboard = (
|
||||||
|
text: string,
|
||||||
|
setCopiedText: (text: string | null) => void
|
||||||
|
) => {
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
setCopiedText(text);
|
setCopiedText(text)
|
||||||
setTimeout(() => setCopiedText(null), 2000);
|
setTimeout(() => setCopiedText(null), 2000)
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const handleSend = async (
|
export const handleSend = async (
|
||||||
input: string,
|
input: string,
|
||||||
@ -66,32 +76,34 @@ export const handleSend = async (
|
|||||||
abortControllerRef: React.MutableRefObject<AbortController | null>,
|
abortControllerRef: React.MutableRefObject<AbortController | null>,
|
||||||
activeFileContent: string
|
activeFileContent: string
|
||||||
) => {
|
) => {
|
||||||
if (input.trim() === '' && !context) return;
|
if (input.trim() === "" && !context) return
|
||||||
|
|
||||||
const newMessage = {
|
const newMessage = {
|
||||||
role: 'user' as const,
|
role: "user" as const,
|
||||||
content: input,
|
content: input,
|
||||||
context: context || undefined
|
context: context || undefined,
|
||||||
};
|
}
|
||||||
const updatedMessages = [...messages, newMessage];
|
const updatedMessages = [...messages, newMessage]
|
||||||
setMessages(updatedMessages);
|
setMessages(updatedMessages)
|
||||||
setInput('');
|
setInput("")
|
||||||
setIsContextExpanded(false);
|
setIsContextExpanded(false)
|
||||||
setIsGenerating(true);
|
setIsGenerating(true)
|
||||||
setIsLoading(true);
|
setIsLoading(true)
|
||||||
|
|
||||||
abortControllerRef.current = new AbortController();
|
abortControllerRef.current = new AbortController()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const anthropicMessages = updatedMessages.map(msg => ({
|
const anthropicMessages = updatedMessages.map((msg) => ({
|
||||||
role: msg.role === 'user' ? 'human' : 'assistant',
|
role: msg.role === "user" ? "human" : "assistant",
|
||||||
content: msg.content
|
content: msg.content,
|
||||||
}));
|
}))
|
||||||
|
|
||||||
const response = await fetch(`${process.env.NEXT_PUBLIC_AI_WORKER_URL}/api`, {
|
const response = await fetch(
|
||||||
method: 'POST',
|
`${process.env.NEXT_PUBLIC_AI_WORKER_URL}/api`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
messages: anthropicMessages,
|
messages: anthropicMessages,
|
||||||
@ -99,64 +111,70 @@ export const handleSend = async (
|
|||||||
activeFileContent: activeFileContent,
|
activeFileContent: activeFileContent,
|
||||||
}),
|
}),
|
||||||
signal: abortControllerRef.current.signal,
|
signal: abortControllerRef.current.signal,
|
||||||
});
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to get AI response');
|
throw new Error("Failed to get AI response")
|
||||||
}
|
}
|
||||||
|
|
||||||
const reader = response.body?.getReader();
|
const reader = response.body?.getReader()
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder()
|
||||||
const assistantMessage = { role: 'assistant' as const, content: '' };
|
const assistantMessage = { role: "assistant" as const, content: "" }
|
||||||
setMessages([...updatedMessages, assistantMessage]);
|
setMessages([...updatedMessages, assistantMessage])
|
||||||
setIsLoading(false);
|
setIsLoading(false)
|
||||||
|
|
||||||
let buffer = '';
|
let buffer = ""
|
||||||
const updateInterval = 100;
|
const updateInterval = 100
|
||||||
let lastUpdateTime = Date.now();
|
let lastUpdateTime = Date.now()
|
||||||
|
|
||||||
if (reader) {
|
if (reader) {
|
||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read()
|
||||||
if (done) break;
|
if (done) break
|
||||||
buffer += decoder.decode(value, { stream: true });
|
buffer += decoder.decode(value, { stream: true })
|
||||||
|
|
||||||
const currentTime = Date.now();
|
const currentTime = Date.now()
|
||||||
if (currentTime - lastUpdateTime > updateInterval) {
|
if (currentTime - lastUpdateTime > updateInterval) {
|
||||||
setMessages(prev => {
|
setMessages((prev) => {
|
||||||
const updatedMessages = [...prev];
|
const updatedMessages = [...prev]
|
||||||
const lastMessage = updatedMessages[updatedMessages.length - 1];
|
const lastMessage = updatedMessages[updatedMessages.length - 1]
|
||||||
lastMessage.content = buffer;
|
lastMessage.content = buffer
|
||||||
return updatedMessages;
|
return updatedMessages
|
||||||
});
|
})
|
||||||
lastUpdateTime = currentTime;
|
lastUpdateTime = currentTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setMessages(prev => {
|
setMessages((prev) => {
|
||||||
const updatedMessages = [...prev];
|
const updatedMessages = [...prev]
|
||||||
const lastMessage = updatedMessages[updatedMessages.length - 1];
|
const lastMessage = updatedMessages[updatedMessages.length - 1]
|
||||||
lastMessage.content = buffer;
|
lastMessage.content = buffer
|
||||||
return updatedMessages;
|
return updatedMessages
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.name === 'AbortError') {
|
if (error.name === "AbortError") {
|
||||||
console.log('Generation aborted');
|
console.log("Generation aborted")
|
||||||
} else {
|
} else {
|
||||||
console.error('Error fetching AI response:', error);
|
console.error("Error fetching AI response:", error)
|
||||||
const errorMessage = { role: 'assistant' as const, content: 'Sorry, I encountered an error. Please try again.' };
|
const errorMessage = {
|
||||||
setMessages(prev => [...prev, errorMessage]);
|
role: "assistant" as const,
|
||||||
|
content: "Sorry, I encountered an error. Please try again.",
|
||||||
|
}
|
||||||
|
setMessages((prev) => [...prev, errorMessage])
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsGenerating(false);
|
setIsGenerating(false)
|
||||||
setIsLoading(false);
|
setIsLoading(false)
|
||||||
abortControllerRef.current = null;
|
abortControllerRef.current = null
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export const handleStopGeneration = (abortControllerRef: React.MutableRefObject<AbortController | null>) => {
|
export const handleStopGeneration = (
|
||||||
|
abortControllerRef: React.MutableRefObject<AbortController | null>
|
||||||
|
) => {
|
||||||
if (abortControllerRef.current) {
|
if (abortControllerRef.current) {
|
||||||
abortControllerRef.current.abort();
|
abortControllerRef.current.abort()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react"
|
|
||||||
import { Button } from "../ui/button"
|
|
||||||
import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react"
|
|
||||||
import { Socket } from "socket.io-client"
|
|
||||||
import { Editor } from "@monaco-editor/react"
|
|
||||||
import { User } from "@/lib/types"
|
import { User } from "@/lib/types"
|
||||||
import { toast } from "sonner"
|
import { Editor } from "@monaco-editor/react"
|
||||||
|
import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react"
|
||||||
import { usePathname, useRouter } from "next/navigation"
|
import { usePathname, useRouter } from "next/navigation"
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
|
import { Socket } from "socket.io-client"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
import { Button } from "../ui/button"
|
||||||
// import monaco from "monaco-editor"
|
// import monaco from "monaco-editor"
|
||||||
|
|
||||||
export default function GenerateInput({
|
export default function GenerateInput({
|
||||||
@ -68,10 +68,12 @@ export default function GenerateInput({
|
|||||||
setCurrentPrompt(input)
|
setCurrentPrompt(input)
|
||||||
socket.emit(
|
socket.emit(
|
||||||
"generateCode",
|
"generateCode",
|
||||||
data.fileName,
|
{
|
||||||
data.code,
|
fileName: data.fileName,
|
||||||
data.line,
|
code: data.code,
|
||||||
regenerate ? currentPrompt : input,
|
line: data.line,
|
||||||
|
instructions: regenerate ? currentPrompt : input
|
||||||
|
},
|
||||||
(res: { response: string; success: boolean }) => {
|
(res: { response: string; success: boolean }) => {
|
||||||
console.log("Generated code", res.response, res.success)
|
console.log("Generated code", res.response, res.success)
|
||||||
// if (!res.success) {
|
// if (!res.success) {
|
||||||
|
@ -91,6 +91,7 @@ export default function CodeEditor({
|
|||||||
|
|
||||||
// Layout state
|
// Layout state
|
||||||
const [isHorizontalLayout, setIsHorizontalLayout] = useState(false)
|
const [isHorizontalLayout, setIsHorizontalLayout] = useState(false)
|
||||||
|
const [previousLayout, setPreviousLayout] = useState(false)
|
||||||
|
|
||||||
// AI Chat state
|
// AI Chat state
|
||||||
const [isAIChatOpen, setIsAIChatOpen] = useState(false)
|
const [isAIChatOpen, setIsAIChatOpen] = useState(false)
|
||||||
@ -106,7 +107,6 @@ export default function CodeEditor({
|
|||||||
|
|
||||||
// Editor state
|
// Editor state
|
||||||
const [editorLanguage, setEditorLanguage] = useState("plaintext")
|
const [editorLanguage, setEditorLanguage] = useState("plaintext")
|
||||||
console.log("editor language: ",editorLanguage)
|
|
||||||
const [cursorLine, setCursorLine] = useState(0)
|
const [cursorLine, setCursorLine] = useState(0)
|
||||||
const [editorRef, setEditorRef] =
|
const [editorRef, setEditorRef] =
|
||||||
useState<monaco.editor.IStandaloneCodeEditor>()
|
useState<monaco.editor.IStandaloneCodeEditor>()
|
||||||
@ -206,7 +206,7 @@ export default function CodeEditor({
|
|||||||
)
|
)
|
||||||
const fetchFileContent = (fileId: string): Promise<string> => {
|
const fetchFileContent = (fileId: string): Promise<string> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
socket?.emit("getFile", fileId, (content: string) => {
|
socket?.emit("getFile", { fileId }, (content: string) => {
|
||||||
resolve(content)
|
resolve(content)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -531,7 +531,7 @@ export default function CodeEditor({
|
|||||||
)
|
)
|
||||||
console.log(`Saving file...${activeFileId}`)
|
console.log(`Saving file...${activeFileId}`)
|
||||||
console.log(`Saving file...${content}`)
|
console.log(`Saving file...${content}`)
|
||||||
socket?.emit("saveFile", activeFileId, content)
|
socket?.emit("saveFile", { fileId: activeFileId, body: content })
|
||||||
}
|
}
|
||||||
}, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000),
|
}, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000),
|
||||||
[socket, fileContents]
|
[socket, fileContents]
|
||||||
@ -548,12 +548,18 @@ export default function CodeEditor({
|
|||||||
setIsAIChatOpen((prev) => !prev)
|
setIsAIChatOpen((prev) => !prev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("keydown", down)
|
document.addEventListener("keydown", down)
|
||||||
|
|
||||||
|
// Added this line to prevent Monaco editor from handling Cmd/Ctrl+L
|
||||||
|
editorRef?.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyL, () => {
|
||||||
|
setIsAIChatOpen((prev) => !prev)
|
||||||
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("keydown", down)
|
document.removeEventListener("keydown", down)
|
||||||
}
|
}
|
||||||
}, [activeFileId, tabs, debouncedSaveData, setIsAIChatOpen])
|
}, [activeFileId, tabs, debouncedSaveData, setIsAIChatOpen, editorRef])
|
||||||
|
|
||||||
// Liveblocks live collaboration setup effect
|
// Liveblocks live collaboration setup effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -642,7 +648,7 @@ export default function CodeEditor({
|
|||||||
|
|
||||||
// Socket event listener effect
|
// Socket event listener effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onConnect = () => {}
|
const onConnect = () => { }
|
||||||
|
|
||||||
const onDisconnect = () => {
|
const onDisconnect = () => {
|
||||||
setTerminals([])
|
setTerminals([])
|
||||||
@ -708,7 +714,7 @@ export default function CodeEditor({
|
|||||||
|
|
||||||
// Debounced function to get file content
|
// Debounced function to get file content
|
||||||
const debouncedGetFile = (tabId: any, callback: any) => {
|
const debouncedGetFile = (tabId: any, callback: any) => {
|
||||||
socket?.emit("getFile", tabId, callback)
|
socket?.emit("getFile", { fileId: tabId }, callback)
|
||||||
} // 300ms debounce delay, adjust as needed
|
} // 300ms debounce delay, adjust as needed
|
||||||
|
|
||||||
const selectFile = (tab: TTab) => {
|
const selectFile = (tab: TTab) => {
|
||||||
@ -828,7 +834,7 @@ export default function CodeEditor({
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
socket?.emit("renameFile", id, newName)
|
socket?.emit("renameFile", { fileId: id, newName })
|
||||||
setTabs((prev) =>
|
setTabs((prev) =>
|
||||||
prev.map((tab) => (tab.id === id ? { ...tab, name: newName } : tab))
|
prev.map((tab) => (tab.id === id ? { ...tab, name: newName } : tab))
|
||||||
)
|
)
|
||||||
@ -837,7 +843,7 @@ export default function CodeEditor({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteFile = (file: TFile) => {
|
const handleDeleteFile = (file: TFile) => {
|
||||||
socket?.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => {
|
socket?.emit("deleteFile", { fileId: file.id }, (response: (TFolder | TFile)[]) => {
|
||||||
setFiles(response)
|
setFiles(response)
|
||||||
})
|
})
|
||||||
closeTab(file.id)
|
closeTab(file.id)
|
||||||
@ -847,11 +853,11 @@ export default function CodeEditor({
|
|||||||
setDeletingFolderId(folder.id)
|
setDeletingFolderId(folder.id)
|
||||||
console.log("deleting folder", folder.id)
|
console.log("deleting folder", folder.id)
|
||||||
|
|
||||||
socket?.emit("getFolder", folder.id, (response: string[]) =>
|
socket?.emit("getFolder", { folderId: folder.id }, (response: string[]) =>
|
||||||
closeTabs(response)
|
closeTabs(response)
|
||||||
)
|
)
|
||||||
|
|
||||||
socket?.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => {
|
socket?.emit("deleteFolder", { folderId: folder.id }, (response: (TFolder | TFile)[]) => {
|
||||||
setFiles(response)
|
setFiles(response)
|
||||||
setDeletingFolderId("")
|
setDeletingFolderId("")
|
||||||
})
|
})
|
||||||
@ -868,8 +874,25 @@ export default function CodeEditor({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toggleLayout = () => {
|
const toggleLayout = () => {
|
||||||
|
if (!isAIChatOpen) {
|
||||||
setIsHorizontalLayout((prev) => !prev)
|
setIsHorizontalLayout((prev) => !prev)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add an effect to handle layout changes when AI chat is opened/closed
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAIChatOpen) {
|
||||||
|
setPreviousLayout(isHorizontalLayout)
|
||||||
|
setIsHorizontalLayout(true)
|
||||||
|
} else {
|
||||||
|
setIsHorizontalLayout(previousLayout)
|
||||||
|
}
|
||||||
|
}, [isAIChatOpen])
|
||||||
|
|
||||||
|
// Modify the toggleAIChat function
|
||||||
|
const toggleAIChat = () => {
|
||||||
|
setIsAIChatOpen((prev) => !prev)
|
||||||
|
}
|
||||||
|
|
||||||
// On disabled access for shared users, show un-interactable loading placeholder + info modal
|
// On disabled access for shared users, show un-interactable loading placeholder + info modal
|
||||||
if (disableAccess.isDisabled)
|
if (disableAccess.isDisabled)
|
||||||
@ -878,7 +901,7 @@ export default function CodeEditor({
|
|||||||
<DisableAccessModal
|
<DisableAccessModal
|
||||||
message={disableAccess.message}
|
message={disableAccess.message}
|
||||||
open={disableAccess.isDisabled}
|
open={disableAccess.isDisabled}
|
||||||
setOpen={() => {}}
|
setOpen={() => { }}
|
||||||
/>
|
/>
|
||||||
<Loading />
|
<Loading />
|
||||||
</>
|
</>
|
||||||
@ -1005,9 +1028,13 @@ export default function CodeEditor({
|
|||||||
setFiles={setFiles}
|
setFiles={setFiles}
|
||||||
addNew={(name, type) => addNew(name, type, setFiles, sandboxData)}
|
addNew={(name, type) => addNew(name, type, setFiles, sandboxData)}
|
||||||
deletingFolderId={deletingFolderId}
|
deletingFolderId={deletingFolderId}
|
||||||
|
toggleAIChat={toggleAIChat}
|
||||||
|
isAIChatOpen={isAIChatOpen}
|
||||||
/>
|
/>
|
||||||
{/* Outer ResizablePanelGroup for main layout */}
|
{/* Outer ResizablePanelGroup for main layout */}
|
||||||
<ResizablePanelGroup direction="horizontal">
|
<ResizablePanelGroup
|
||||||
|
direction={isHorizontalLayout ? "horizontal" : "vertical"}
|
||||||
|
>
|
||||||
{/* Left side: Editor and Preview/Terminal */}
|
{/* Left side: Editor and Preview/Terminal */}
|
||||||
<ResizablePanel defaultSize={isAIChatOpen ? 80 : 100} minSize={50}>
|
<ResizablePanel defaultSize={isAIChatOpen ? 80 : 100} minSize={50}>
|
||||||
<ResizablePanelGroup
|
<ResizablePanelGroup
|
||||||
@ -1136,6 +1163,7 @@ export default function CodeEditor({
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="mr-2 border"
|
className="mr-2 border"
|
||||||
|
disabled={isAIChatOpen}
|
||||||
>
|
>
|
||||||
{isHorizontalLayout ? (
|
{isHorizontalLayout ? (
|
||||||
<ArrowRightToLine className="w-4 h-4" />
|
<ArrowRightToLine className="w-4 h-4" />
|
||||||
@ -1190,6 +1218,7 @@ export default function CodeEditor({
|
|||||||
tabs.find((tab) => tab.id === activeFileId)?.name ||
|
tabs.find((tab) => tab.id === activeFileId)?.name ||
|
||||||
"No file selected"
|
"No file selected"
|
||||||
}
|
}
|
||||||
|
onClose={toggleAIChat}
|
||||||
/>
|
/>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
</>
|
</>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { useOthers } from "@/liveblocks.config";
|
import { useOthers } from "@/liveblocks.config"
|
||||||
|
|
||||||
const classNames = {
|
const classNames = {
|
||||||
red: "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-red-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-red-950 to-red-600 flex items-center justify-center text-xs font-medium",
|
red: "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-red-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-red-950 to-red-600 flex items-center justify-center text-xs font-medium",
|
||||||
@ -14,10 +14,10 @@ const classNames = {
|
|||||||
purple:
|
purple:
|
||||||
"w-8 h-8 leading-none font-mono rounded-full ring-1 ring-purple-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-purple-950 to-purple-600 flex items-center justify-center text-xs font-medium",
|
"w-8 h-8 leading-none font-mono rounded-full ring-1 ring-purple-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-purple-950 to-purple-600 flex items-center justify-center text-xs font-medium",
|
||||||
pink: "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-pink-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-pink-950 to-pink-600 flex items-center justify-center text-xs font-medium",
|
pink: "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-pink-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-pink-950 to-pink-600 flex items-center justify-center text-xs font-medium",
|
||||||
};
|
}
|
||||||
|
|
||||||
export function Avatars() {
|
export function Avatars() {
|
||||||
const users = useOthers();
|
const users = useOthers()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -30,12 +30,12 @@ export function Avatars() {
|
|||||||
.slice(0, 2)
|
.slice(0, 2)
|
||||||
.map((letter) => letter[0].toUpperCase())}
|
.map((letter) => letter[0].toUpperCase())}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{users.length > 0 ? (
|
{users.length > 0 ? (
|
||||||
<div className="h-full w-[1px] bg-border mx-2" />
|
<div className="h-full w-[1px] bg-border mx-2" />
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import { useEffect, useMemo, useState } from "react"
|
import { colors } from "@/lib/colors"
|
||||||
import {
|
import {
|
||||||
AwarenessList,
|
AwarenessList,
|
||||||
TypedLiveblocksProvider,
|
TypedLiveblocksProvider,
|
||||||
UserAwareness,
|
UserAwareness,
|
||||||
useSelf,
|
|
||||||
} from "@/liveblocks.config"
|
} from "@/liveblocks.config"
|
||||||
import { colors } from "@/lib/colors"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
|
||||||
export function Cursors({
|
export function Cursors({
|
||||||
yProvider,
|
yProvider,
|
||||||
|
@ -1,43 +1,35 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
} from "@/components/ui/dialog"
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
|
|
||||||
import {
|
import { Loader2 } from "lucide-react"
|
||||||
ChevronRight,
|
import { useRouter } from "next/navigation"
|
||||||
FileStack,
|
import { useEffect } from "react"
|
||||||
Globe,
|
|
||||||
Loader2,
|
|
||||||
TextCursor,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
export default function DisableAccessModal({
|
export default function DisableAccessModal({
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
message,
|
message,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void
|
||||||
message: string;
|
message: string
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
router.push("/dashboard");
|
router.push("/dashboard")
|
||||||
}, 5000);
|
}, 5000)
|
||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout)
|
||||||
}
|
}
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
@ -54,5 +46,5 @@ export default function DisableAccessModal({
|
|||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { RoomProvider } from "@/liveblocks.config";
|
import { RoomProvider } from "@/liveblocks.config"
|
||||||
import { ClientSideSuspense } from "@liveblocks/react";
|
|
||||||
|
|
||||||
export function Room({
|
export function Room({
|
||||||
id,
|
id,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<RoomProvider
|
<RoomProvider
|
||||||
@ -21,5 +20,5 @@ export function Room({
|
|||||||
{children}
|
{children}
|
||||||
{/* </ClientSideSuspense> */}
|
{/* </ClientSideSuspense> */}
|
||||||
</RoomProvider>
|
</RoomProvider>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import Image from "next/image"
|
|
||||||
import Logo from "@/assets/logo.svg"
|
import Logo from "@/assets/logo.svg"
|
||||||
import { Skeleton } from "@/components/ui/skeleton"
|
|
||||||
import { Loader2, X } from "lucide-react"
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -11,6 +8,9 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
|
import { Loader2, X } from "lucide-react"
|
||||||
|
import Image from "next/image"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
export default function Loading({
|
export default function Loading({
|
||||||
|
@ -1,34 +1,38 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react";
|
import { Button } from "@/components/ui/button"
|
||||||
import { Button } from "@/components/ui/button";
|
import {
|
||||||
import { useTerminal } from "@/context/TerminalContext";
|
Popover,
|
||||||
import { Play, Pause, Globe, Globe2 } from "lucide-react";
|
PopoverContent,
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
PopoverTrigger,
|
||||||
import { Sandbox, User } from "@/lib/types";
|
} from "@/components/ui/popover"
|
||||||
|
import { useTerminal } from "@/context/TerminalContext"
|
||||||
|
import { Sandbox, User } from "@/lib/types"
|
||||||
|
import { Globe } from "lucide-react"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
export default function DeployButtonModal({
|
export default function DeployButtonModal({
|
||||||
userData,
|
userData,
|
||||||
data,
|
data,
|
||||||
}: {
|
}: {
|
||||||
userData: User;
|
userData: User
|
||||||
data: Sandbox;
|
data: Sandbox
|
||||||
}) {
|
}) {
|
||||||
const { deploy } = useTerminal();
|
const { deploy } = useTerminal()
|
||||||
const [isDeploying, setIsDeploying] = useState(false);
|
const [isDeploying, setIsDeploying] = useState(false)
|
||||||
|
|
||||||
const handleDeploy = () => {
|
const handleDeploy = () => {
|
||||||
if (isDeploying) {
|
if (isDeploying) {
|
||||||
console.log("Stopping deployment...");
|
console.log("Stopping deployment...")
|
||||||
setIsDeploying(false);
|
setIsDeploying(false)
|
||||||
} else {
|
} else {
|
||||||
console.log("Starting deployment...");
|
console.log("Starting deployment...")
|
||||||
setIsDeploying(true);
|
setIsDeploying(true)
|
||||||
deploy(() => {
|
deploy(() => {
|
||||||
setIsDeploying(false);
|
setIsDeploying(false)
|
||||||
});
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -39,7 +43,10 @@ export default function DeployButtonModal({
|
|||||||
Deploy
|
Deploy
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</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" style={{ backgroundColor: 'rgb(10,10,10)', color: 'white' }}>
|
<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"
|
||||||
|
style={{ backgroundColor: "rgb(10,10,10)", color: "white" }}
|
||||||
|
>
|
||||||
<h3 className="font-semibold text-gray-300 mb-2">Domains</h3>
|
<h3 className="font-semibold text-gray-300 mb-2">Domains</h3>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<DeploymentOption
|
<DeploymentOption
|
||||||
@ -49,16 +56,30 @@ export default function DeployButtonModal({
|
|||||||
user={userData.name}
|
user={userData.name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" className="mt-4 w-full bg-[#0a0a0a] text-white hover:bg-[#262626]" onClick={handleDeploy}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="mt-4 w-full bg-[#0a0a0a] text-white hover:bg-[#262626]"
|
||||||
|
onClick={handleDeploy}
|
||||||
|
>
|
||||||
{isDeploying ? "Deploying..." : "Update"}
|
{isDeploying ? "Deploying..." : "Update"}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DeploymentOption({ icon, domain, timestamp, user }: { icon: React.ReactNode; domain: string; timestamp: string; user: string }) {
|
function DeploymentOption({
|
||||||
|
icon,
|
||||||
|
domain,
|
||||||
|
timestamp,
|
||||||
|
user,
|
||||||
|
}: {
|
||||||
|
icon: React.ReactNode
|
||||||
|
domain: string
|
||||||
|
timestamp: string
|
||||||
|
user: string
|
||||||
|
}) {
|
||||||
return (
|
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 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 items-start gap-2 relative">
|
||||||
@ -72,7 +93,9 @@ function DeploymentOption({ icon, domain, timestamp, user }: { icon: React.React
|
|||||||
{domain}
|
{domain}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-400 mt-0 ml-7">{timestamp} • {user}</p>
|
<p className="text-sm text-gray-400 mt-0 ml-7">
|
||||||
|
{timestamp} • {user}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,60 +1,57 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
} from "@/components/ui/dialog"
|
||||||
} from "@/components/ui/dialog";
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { z } from "zod";
|
import { useForm } from "react-hook-form"
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { z } from "zod"
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form"
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input"
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select"
|
||||||
import { Loader2 } from "lucide-react";
|
import { deleteSandbox, updateSandbox } from "@/lib/actions"
|
||||||
import { useState } from "react";
|
import { Sandbox } from "@/lib/types"
|
||||||
import { Sandbox } from "@/lib/types";
|
import { Loader2 } from "lucide-react"
|
||||||
import { Button } from "@/components/ui/button";
|
import { useRouter } from "next/navigation"
|
||||||
import { deleteSandbox, updateSandbox } from "@/lib/actions";
|
import { useState } from "react"
|
||||||
import { useRouter } from "next/navigation";
|
import { toast } from "sonner"
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: z.string().min(1).max(16),
|
name: z.string().min(1).max(16),
|
||||||
visibility: z.enum(["public", "private"]),
|
visibility: z.enum(["public", "private"]),
|
||||||
});
|
})
|
||||||
|
|
||||||
export default function EditSandboxModal({
|
export default function EditSandboxModal({
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
data,
|
data,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void
|
||||||
data: Sandbox;
|
data: Sandbox
|
||||||
}) {
|
}) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false)
|
||||||
const [loadingDelete, setLoadingDelete] = useState(false);
|
const [loadingDelete, setLoadingDelete] = useState(false)
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
@ -62,22 +59,22 @@ export default function EditSandboxModal({
|
|||||||
name: data.name,
|
name: data.name,
|
||||||
visibility: data.visibility,
|
visibility: data.visibility,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
setLoading(true);
|
setLoading(true)
|
||||||
await updateSandbox({ id: data.id, ...values });
|
await updateSandbox({ id: data.id, ...values })
|
||||||
|
|
||||||
toast.success("Sandbox updated successfully");
|
toast.success("Sandbox updated successfully")
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onDelete() {
|
async function onDelete() {
|
||||||
setLoadingDelete(true);
|
setLoadingDelete(true)
|
||||||
await deleteSandbox(data.id);
|
await deleteSandbox(data.id)
|
||||||
|
|
||||||
router.push("/dashboard");
|
router.push("/dashboard")
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -153,5 +150,5 @@ export default function EditSandboxModal({
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,73 +1,78 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import React, { useEffect, useRef } from 'react';
|
import { Button } from "@/components/ui/button"
|
||||||
import { Play, StopCircle } from "lucide-react";
|
import { usePreview } from "@/context/PreviewContext"
|
||||||
import { Button } from "@/components/ui/button";
|
import { useTerminal } from "@/context/TerminalContext"
|
||||||
import { useTerminal } from "@/context/TerminalContext";
|
import { Sandbox } from "@/lib/types"
|
||||||
import { usePreview } from "@/context/PreviewContext";
|
import { Play, StopCircle } from "lucide-react"
|
||||||
import { toast } from "sonner";
|
import { useEffect, useRef } from "react"
|
||||||
import { Sandbox } from "@/lib/types";
|
import { toast } from "sonner"
|
||||||
|
|
||||||
export default function RunButtonModal({
|
export default function RunButtonModal({
|
||||||
isRunning,
|
isRunning,
|
||||||
setIsRunning,
|
setIsRunning,
|
||||||
sandboxData,
|
sandboxData,
|
||||||
}: {
|
}: {
|
||||||
isRunning: boolean;
|
isRunning: boolean
|
||||||
setIsRunning: (running: boolean) => void;
|
setIsRunning: (running: boolean) => void
|
||||||
sandboxData: Sandbox;
|
sandboxData: Sandbox
|
||||||
}) {
|
}) {
|
||||||
const { createNewTerminal, closeTerminal, terminals } = useTerminal();
|
const { createNewTerminal, closeTerminal, terminals } = useTerminal()
|
||||||
const { setIsPreviewCollapsed, previewPanelRef } = usePreview();
|
const { setIsPreviewCollapsed, previewPanelRef } = usePreview()
|
||||||
// Ref to keep track of the last created terminal's ID
|
// Ref to keep track of the last created terminal's ID
|
||||||
const lastCreatedTerminalRef = useRef<string | null>(null);
|
const lastCreatedTerminalRef = useRef<string | null>(null)
|
||||||
|
|
||||||
// Effect to update the lastCreatedTerminalRef when a new terminal is added
|
// Effect to update the lastCreatedTerminalRef when a new terminal is added
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (terminals.length > 0 && !isRunning) {
|
if (terminals.length > 0 && !isRunning) {
|
||||||
const latestTerminal = terminals[terminals.length - 1];
|
const latestTerminal = terminals[terminals.length - 1]
|
||||||
if (latestTerminal && latestTerminal.id !== lastCreatedTerminalRef.current) {
|
if (
|
||||||
lastCreatedTerminalRef.current = latestTerminal.id;
|
latestTerminal &&
|
||||||
|
latestTerminal.id !== lastCreatedTerminalRef.current
|
||||||
|
) {
|
||||||
|
lastCreatedTerminalRef.current = latestTerminal.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [terminals, isRunning]);
|
}, [terminals, isRunning])
|
||||||
|
|
||||||
const handleRun = async () => {
|
const handleRun = async () => {
|
||||||
if (isRunning && lastCreatedTerminalRef.current)
|
if (isRunning && lastCreatedTerminalRef.current) {
|
||||||
{
|
await closeTerminal(lastCreatedTerminalRef.current)
|
||||||
await closeTerminal(lastCreatedTerminalRef.current);
|
lastCreatedTerminalRef.current = null
|
||||||
lastCreatedTerminalRef.current = null;
|
setIsPreviewCollapsed(true)
|
||||||
setIsPreviewCollapsed(true);
|
previewPanelRef.current?.collapse()
|
||||||
previewPanelRef.current?.collapse();
|
} else if (!isRunning && terminals.length < 4) {
|
||||||
}
|
const command =
|
||||||
else if (!isRunning && terminals.length < 4)
|
sandboxData.type === "streamlit"
|
||||||
{
|
|
||||||
const command = sandboxData.type === "streamlit"
|
|
||||||
? "pip install -r requirements.txt && streamlit run main.py --server.runOnSave true"
|
? "pip install -r requirements.txt && streamlit run main.py --server.runOnSave true"
|
||||||
: "yarn install && yarn dev";
|
: "yarn install && yarn dev"
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create a new terminal with the appropriate command
|
// Create a new terminal with the appropriate command
|
||||||
await createNewTerminal(command);
|
await createNewTerminal(command)
|
||||||
setIsPreviewCollapsed(false);
|
setIsPreviewCollapsed(false)
|
||||||
previewPanelRef.current?.expand();
|
previewPanelRef.current?.expand()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Failed to create new terminal.");
|
toast.error("Failed to create new terminal.")
|
||||||
console.error("Error creating new terminal:", error);
|
console.error("Error creating new terminal:", error)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
} else if (!isRunning) {
|
} else if (!isRunning) {
|
||||||
toast.error("You've reached the maximum number of terminals.");
|
toast.error("You've reached the maximum number of terminals.")
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsRunning(!isRunning);
|
setIsRunning(!isRunning)
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button variant="outline" onClick={handleRun}>
|
<Button variant="outline" onClick={handleRun}>
|
||||||
{isRunning ? <StopCircle className="w-4 h-4 mr-2" /> : <Play className="w-4 h-4 mr-2" />}
|
{isRunning ? (
|
||||||
{isRunning ? 'Stop' : 'Run'}
|
<StopCircle className="w-4 h-4 mr-2" />
|
||||||
|
) : (
|
||||||
|
<Play className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
{isRunning ? "Stop" : "Run"}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
)
|
||||||
}
|
}
|
@ -6,10 +6,11 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { z } from "zod"
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@ -18,14 +19,13 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form"
|
} from "@/components/ui/form"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Link, Loader2, UserPlus, X } from "lucide-react"
|
|
||||||
import { useState } from "react"
|
|
||||||
import { Sandbox } from "@/lib/types"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { shareSandbox } from "@/lib/actions"
|
import { shareSandbox } from "@/lib/actions"
|
||||||
|
import { Sandbox } from "@/lib/types"
|
||||||
|
import { DialogDescription } from "@radix-ui/react-dialog"
|
||||||
|
import { Link, Loader2, UserPlus } from "lucide-react"
|
||||||
|
import { useState } from "react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import SharedUser from "./sharedUser"
|
import SharedUser from "./sharedUser"
|
||||||
import { DialogDescription } from "@radix-ui/react-dialog"
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
|
@ -1,30 +1,33 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { Link, RotateCw, UnfoldVertical } from "lucide-react"
|
||||||
import {
|
import {
|
||||||
Link,
|
forwardRef,
|
||||||
RotateCw,
|
useEffect,
|
||||||
TerminalSquare,
|
useImperativeHandle,
|
||||||
UnfoldVertical,
|
useRef,
|
||||||
} from "lucide-react"
|
useState,
|
||||||
import { useEffect, useRef, useState, useImperativeHandle, forwardRef } from "react"
|
} from "react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
export default forwardRef(function PreviewWindow({
|
export default forwardRef(function PreviewWindow(
|
||||||
|
{
|
||||||
collapsed,
|
collapsed,
|
||||||
open,
|
open,
|
||||||
src
|
src,
|
||||||
}: {
|
}: {
|
||||||
collapsed: boolean
|
collapsed: boolean
|
||||||
open: () => void
|
open: () => void
|
||||||
src: string
|
src: string
|
||||||
},
|
},
|
||||||
ref: React.Ref<{
|
ref: React.Ref<{
|
||||||
refreshIframe: () => void
|
refreshIframe: () => void
|
||||||
}>) {
|
}>
|
||||||
|
) {
|
||||||
const frameRef = useRef<HTMLIFrameElement>(null)
|
const frameRef = useRef<HTMLIFrameElement>(null)
|
||||||
const [iframeKey, setIframeKey] = useState(0)
|
const [iframeKey, setIframeKey] = useState(0)
|
||||||
const refreshIframe = () => {
|
const refreshIframe = () => {
|
||||||
setIframeKey(prev => prev + 1)
|
setIframeKey((prev) => prev + 1)
|
||||||
}
|
}
|
||||||
// Refresh the preview when the URL changes.
|
// Refresh the preview when the URL changes.
|
||||||
useEffect(refreshIframe, [src])
|
useEffect(refreshIframe, [src])
|
||||||
@ -76,7 +79,8 @@ function PreviewButton({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${disabled ? "pointer-events-none opacity-50" : ""
|
className={`${
|
||||||
|
disabled ? "pointer-events-none opacity-50" : ""
|
||||||
} p-0.5 h-5 w-5 ml-0.5 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm`}
|
} p-0.5 h-5 w-5 ml-0.5 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm`}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import Image from "next/image";
|
|
||||||
import { getIconForFile } from "vscode-icons-js";
|
|
||||||
import { TFile, TTab } from "@/lib/types";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuContent,
|
ContextMenuContent,
|
||||||
ContextMenuItem,
|
ContextMenuItem,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from "@/components/ui/context-menu";
|
} from "@/components/ui/context-menu"
|
||||||
import { Loader2, Pencil, Trash2 } from "lucide-react";
|
import { TFile, TTab } from "@/lib/types"
|
||||||
|
import { Loader2, Pencil, Trash2 } from "lucide-react"
|
||||||
|
import Image from "next/image"
|
||||||
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
import { getIconForFile } from "vscode-icons-js"
|
||||||
|
|
||||||
import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"
|
||||||
|
|
||||||
export default function SidebarFile({
|
export default function SidebarFile({
|
||||||
data,
|
data,
|
||||||
@ -22,36 +22,36 @@ export default function SidebarFile({
|
|||||||
movingId,
|
movingId,
|
||||||
deletingFolderId,
|
deletingFolderId,
|
||||||
}: {
|
}: {
|
||||||
data: TFile;
|
data: TFile
|
||||||
selectFile: (file: TTab) => void;
|
selectFile: (file: TTab) => void
|
||||||
handleRename: (
|
handleRename: (
|
||||||
id: string,
|
id: string,
|
||||||
newName: string,
|
newName: string,
|
||||||
oldName: string,
|
oldName: string,
|
||||||
type: "file" | "folder"
|
type: "file" | "folder"
|
||||||
) => boolean;
|
) => boolean
|
||||||
handleDeleteFile: (file: TFile) => void;
|
handleDeleteFile: (file: TFile) => void
|
||||||
movingId: string;
|
movingId: string
|
||||||
deletingFolderId: string;
|
deletingFolderId: string
|
||||||
}) {
|
}) {
|
||||||
const isMoving = movingId === data.id;
|
const isMoving = movingId === data.id
|
||||||
const isDeleting =
|
const isDeleting =
|
||||||
deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId);
|
deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId)
|
||||||
|
|
||||||
const ref = useRef(null); // for draggable
|
const ref = useRef(null) // for draggable
|
||||||
const [dragging, setDragging] = useState(false);
|
const [dragging, setDragging] = useState(false)
|
||||||
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const [imgSrc, setImgSrc] = useState(`/icons/${getIconForFile(data.name)}`);
|
const [imgSrc, setImgSrc] = useState(`/icons/${getIconForFile(data.name)}`)
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false)
|
||||||
const [pendingDelete, setPendingDelete] = useState(isDeleting);
|
const [pendingDelete, setPendingDelete] = useState(isDeleting)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPendingDelete(isDeleting);
|
setPendingDelete(isDeleting)
|
||||||
}, [isDeleting]);
|
}, [isDeleting])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = ref.current;
|
const el = ref.current
|
||||||
|
|
||||||
if (el)
|
if (el)
|
||||||
return draggable({
|
return draggable({
|
||||||
@ -59,14 +59,14 @@ export default function SidebarFile({
|
|||||||
onDragStart: () => setDragging(true),
|
onDragStart: () => setDragging(true),
|
||||||
onDrop: () => setDragging(false),
|
onDrop: () => setDragging(false),
|
||||||
getInitialData: () => ({ id: data.id }),
|
getInitialData: () => ({ id: data.id }),
|
||||||
});
|
})
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editing) {
|
if (editing) {
|
||||||
setTimeout(() => inputRef.current?.focus(), 0);
|
setTimeout(() => inputRef.current?.focus(), 0)
|
||||||
}
|
}
|
||||||
}, [editing, inputRef.current]);
|
}, [editing, inputRef.current])
|
||||||
|
|
||||||
const renameFile = () => {
|
const renameFile = () => {
|
||||||
const renamed = handleRename(
|
const renamed = handleRename(
|
||||||
@ -74,12 +74,12 @@ export default function SidebarFile({
|
|||||||
inputRef.current?.value ?? data.name,
|
inputRef.current?.value ?? data.name,
|
||||||
data.name,
|
data.name,
|
||||||
"file"
|
"file"
|
||||||
);
|
)
|
||||||
if (!renamed && inputRef.current) {
|
if (!renamed && inputRef.current) {
|
||||||
inputRef.current.value = data.name;
|
inputRef.current.value = data.name
|
||||||
|
}
|
||||||
|
setEditing(false)
|
||||||
}
|
}
|
||||||
setEditing(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
@ -88,7 +88,7 @@ export default function SidebarFile({
|
|||||||
disabled={pendingDelete || dragging || isMoving}
|
disabled={pendingDelete || dragging || isMoving}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!editing && !pendingDelete && !isMoving)
|
if (!editing && !pendingDelete && !isMoving)
|
||||||
selectFile({ ...data, saved: true });
|
selectFile({ ...data, saved: true })
|
||||||
}}
|
}}
|
||||||
onDoubleClick={() => {
|
onDoubleClick={() => {
|
||||||
setEditing(true)
|
setEditing(true)
|
||||||
@ -119,8 +119,8 @@ export default function SidebarFile({
|
|||||||
) : (
|
) : (
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
renameFile();
|
renameFile()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@ -138,8 +138,8 @@ export default function SidebarFile({
|
|||||||
<ContextMenuContent>
|
<ContextMenuContent>
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log("rename");
|
console.log("rename")
|
||||||
setEditing(true);
|
setEditing(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Pencil className="w-4 h-4 mr-2" />
|
<Pencil className="w-4 h-4 mr-2" />
|
||||||
@ -148,9 +148,9 @@ export default function SidebarFile({
|
|||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
disabled={pendingDelete}
|
disabled={pendingDelete}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log("delete");
|
console.log("delete")
|
||||||
setPendingDelete(true);
|
setPendingDelete(true)
|
||||||
handleDeleteFile(data);
|
handleDeleteFile(data)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4 mr-2" />
|
<Trash2 className="w-4 h-4 mr-2" />
|
||||||
@ -158,5 +158,5 @@ export default function SidebarFile({
|
|||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import Image from "next/image"
|
|
||||||
import { useEffect, useRef, useState } from "react"
|
|
||||||
import { getIconForFolder, getIconForOpenFolder } from "vscode-icons-js"
|
|
||||||
import { TFile, TFolder, TTab } from "@/lib/types"
|
|
||||||
import SidebarFile from "./file"
|
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuContent,
|
ContextMenuContent,
|
||||||
ContextMenuItem,
|
ContextMenuItem,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from "@/components/ui/context-menu"
|
} from "@/components/ui/context-menu"
|
||||||
import { ChevronRight, Loader2, Pencil, Trash2 } from "lucide-react"
|
import { TFile, TFolder, TTab } from "@/lib/types"
|
||||||
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { motion, AnimatePresence } from "framer-motion"
|
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"
|
||||||
|
import { AnimatePresence, motion } from "framer-motion"
|
||||||
|
import { ChevronRight, Pencil, Trash2 } from "lucide-react"
|
||||||
|
import Image from "next/image"
|
||||||
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
import { getIconForFolder, getIconForOpenFolder } from "vscode-icons-js"
|
||||||
|
import SidebarFile from "./file"
|
||||||
|
|
||||||
// Note: Renaming has not been implemented in the backend yet, so UI relating to renaming is commented out
|
// Note: Renaming has not been implemented in the backend yet, so UI relating to renaming is commented out
|
||||||
|
|
||||||
|
@ -1,28 +1,20 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import {
|
import { Sandbox, TFile, TFolder, TTab } from "@/lib/types"
|
||||||
FilePlus,
|
import { FilePlus, FolderPlus, MessageSquareMore, Sparkles } from "lucide-react"
|
||||||
FolderPlus,
|
import { useEffect, useMemo, useRef, useState } from "react"
|
||||||
Loader2,
|
import { Socket } from "socket.io-client"
|
||||||
MonitorPlay,
|
|
||||||
Search,
|
|
||||||
Sparkles,
|
|
||||||
} from "lucide-react"
|
|
||||||
import SidebarFile from "./file"
|
import SidebarFile from "./file"
|
||||||
import SidebarFolder from "./folder"
|
import SidebarFolder from "./folder"
|
||||||
import { Sandbox, TFile, TFolder, TTab } from "@/lib/types"
|
|
||||||
import { useEffect, useMemo, useRef, useState } from "react"
|
|
||||||
import New from "./new"
|
import New from "./new"
|
||||||
import { Socket } from "socket.io-client"
|
|
||||||
import { Switch } from "@/components/ui/switch"
|
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
|
import { cn, sortFileExplorer } from "@/lib/utils"
|
||||||
import {
|
import {
|
||||||
dropTargetForElements,
|
dropTargetForElements,
|
||||||
monitorForElements,
|
monitorForElements,
|
||||||
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter"
|
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter"
|
||||||
import Button from "@/components/ui/customButton"
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton"
|
|
||||||
import { sortFileExplorer } from "@/lib/utils"
|
|
||||||
|
|
||||||
export default function Sidebar({
|
export default function Sidebar({
|
||||||
sandboxData,
|
sandboxData,
|
||||||
@ -35,6 +27,8 @@ export default function Sidebar({
|
|||||||
setFiles,
|
setFiles,
|
||||||
addNew,
|
addNew,
|
||||||
deletingFolderId,
|
deletingFolderId,
|
||||||
|
toggleAIChat,
|
||||||
|
isAIChatOpen,
|
||||||
}: {
|
}: {
|
||||||
sandboxData: Sandbox
|
sandboxData: Sandbox
|
||||||
files: (TFile | TFolder)[]
|
files: (TFile | TFolder)[]
|
||||||
@ -51,6 +45,8 @@ export default function Sidebar({
|
|||||||
setFiles: (files: (TFile | TFolder)[]) => void
|
setFiles: (files: (TFile | TFolder)[]) => void
|
||||||
addNew: (name: string, type: "file" | "folder") => void
|
addNew: (name: string, type: "file" | "folder") => void
|
||||||
deletingFolderId: string
|
deletingFolderId: string
|
||||||
|
toggleAIChat: () => void
|
||||||
|
isAIChatOpen: boolean
|
||||||
}) {
|
}) {
|
||||||
const ref = useRef(null) // drop target
|
const ref = useRef(null) // drop target
|
||||||
|
|
||||||
@ -95,8 +91,10 @@ export default function Sidebar({
|
|||||||
setMovingId(fileId)
|
setMovingId(fileId)
|
||||||
socket.emit(
|
socket.emit(
|
||||||
"moveFile",
|
"moveFile",
|
||||||
|
{
|
||||||
fileId,
|
fileId,
|
||||||
folderId,
|
folderId
|
||||||
|
},
|
||||||
(response: (TFolder | TFile)[]) => {
|
(response: (TFolder | TFile)[]) => {
|
||||||
setFiles(response)
|
setFiles(response)
|
||||||
setMovingId("")
|
setMovingId("")
|
||||||
@ -107,9 +105,9 @@ export default function Sidebar({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-56 select-none flex flex-col text-sm items-start justify-between p-2">
|
<div className="h-full w-56 select-none flex flex-col text-sm">
|
||||||
<div className="w-full flex flex-col items-start">
|
<div className="flex-grow overflow-auto p-2 pb-[84px]">
|
||||||
<div className="flex w-full items-center justify-between h-8 mb-1 ">
|
<div className="flex w-full items-center justify-between h-8 mb-1">
|
||||||
<div className="text-muted-foreground">Explorer</div>
|
<div className="text-muted-foreground">Explorer</div>
|
||||||
<div className="flex space-x-1">
|
<div className="flex space-x-1">
|
||||||
<button
|
<button
|
||||||
@ -185,10 +183,49 @@ export default function Sidebar({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full space-y-4">
|
<div className="fixed bottom-0 w-48 flex flex-col p-2 bg-background">
|
||||||
{/* <Button className="w-full">
|
<Button
|
||||||
<MonitorPlay className="w-4 h-4 mr-2" /> Run
|
variant="ghost"
|
||||||
</Button> */}
|
className="w-full justify-start text-sm text-muted-foreground font-normal h-8 px-2 mb-2"
|
||||||
|
disabled
|
||||||
|
aria-disabled="true"
|
||||||
|
style={{ opacity: 1 }}
|
||||||
|
>
|
||||||
|
<Sparkles className="h-4 w-4 mr-2 text-indigo-500 opacity-70" />
|
||||||
|
AI Editor
|
||||||
|
<div className="ml-auto">
|
||||||
|
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground">
|
||||||
|
<span className="text-xs">⌘</span>G
|
||||||
|
</kbd>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-start text-sm font-normal h-8 px-2 mb-2 border-t",
|
||||||
|
isAIChatOpen
|
||||||
|
? "bg-muted-foreground/25 text-foreground"
|
||||||
|
: "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
onClick={toggleAIChat}
|
||||||
|
aria-disabled={false}
|
||||||
|
style={{ opacity: 1 }}
|
||||||
|
>
|
||||||
|
<MessageSquareMore
|
||||||
|
className={cn(
|
||||||
|
"h-4 w-4 mr-2",
|
||||||
|
isAIChatOpen
|
||||||
|
? "text-indigo-500"
|
||||||
|
: "text-indigo-500 opacity-70"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
AI Chat
|
||||||
|
<div className="ml-auto">
|
||||||
|
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground">
|
||||||
|
<span className="text-xs">⌘</span>L
|
||||||
|
</kbd>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { validateName } from "@/lib/utils";
|
import { validateName } from "@/lib/utils"
|
||||||
import Image from "next/image";
|
import Image from "next/image"
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react"
|
||||||
import { Socket } from "socket.io-client";
|
import { Socket } from "socket.io-client"
|
||||||
|
|
||||||
export default function New({
|
export default function New({
|
||||||
socket,
|
socket,
|
||||||
@ -11,42 +11,42 @@ export default function New({
|
|||||||
stopEditing,
|
stopEditing,
|
||||||
addNew,
|
addNew,
|
||||||
}: {
|
}: {
|
||||||
socket: Socket;
|
socket: Socket
|
||||||
type: "file" | "folder";
|
type: "file" | "folder"
|
||||||
stopEditing: () => void;
|
stopEditing: () => void
|
||||||
addNew: (name: string, type: "file" | "folder") => void;
|
addNew: (name: string, type: "file" | "folder") => void
|
||||||
}) {
|
}) {
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
function createNew() {
|
function createNew() {
|
||||||
const name = inputRef.current?.value;
|
const name = inputRef.current?.value
|
||||||
|
|
||||||
if (name) {
|
if (name) {
|
||||||
const valid = validateName(name, "", type);
|
const valid = validateName(name, "", type)
|
||||||
if (valid.status) {
|
if (valid.status) {
|
||||||
if (type === "file") {
|
if (type === "file") {
|
||||||
socket.emit(
|
socket.emit(
|
||||||
"createFile",
|
"createFile",
|
||||||
name,
|
{ name },
|
||||||
({ success }: { success: boolean }) => {
|
({ success }: { success: boolean }) => {
|
||||||
if (success) {
|
if (success) {
|
||||||
addNew(name, type);
|
addNew(name, type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
} else {
|
} else {
|
||||||
socket.emit("createFolder", name, () => {
|
socket.emit("createFolder", { name }, () => {
|
||||||
addNew(name, type);
|
addNew(name, type)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stopEditing();
|
stopEditing()
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus()
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex items-center h-7 px-1 hover:bg-secondary rounded-sm cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring">
|
<div className="w-full flex items-center h-7 px-1 hover:bg-secondary rounded-sm cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring">
|
||||||
@ -63,8 +63,8 @@ export default function New({
|
|||||||
/>
|
/>
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
createNew();
|
createNew()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@ -74,5 +74,5 @@ export default function New({
|
|||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button"
|
||||||
import Tab from "@/components/ui/tab";
|
import Tab from "@/components/ui/tab"
|
||||||
import { Terminal } from "@xterm/xterm";
|
|
||||||
import { Loader2, Plus, SquareTerminal, TerminalSquare } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import EditorTerminal from "./terminal";
|
|
||||||
import { useTerminal } from "@/context/TerminalContext";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useSocket } from "@/context/SocketContext"
|
import { useSocket } from "@/context/SocketContext"
|
||||||
|
import { useTerminal } from "@/context/TerminalContext"
|
||||||
|
import { Terminal } from "@xterm/xterm"
|
||||||
|
import { Loader2, Plus, SquareTerminal, TerminalSquare } from "lucide-react"
|
||||||
|
import { useEffect } from "react"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
import EditorTerminal from "./terminal"
|
||||||
|
|
||||||
export default function Terminals() {
|
export default function Terminals() {
|
||||||
|
const { socket } = useSocket()
|
||||||
const { socket } = useSocket();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
terminals,
|
terminals,
|
||||||
@ -22,24 +21,24 @@ export default function Terminals() {
|
|||||||
activeTerminalId,
|
activeTerminalId,
|
||||||
setActiveTerminalId,
|
setActiveTerminalId,
|
||||||
creatingTerminal,
|
creatingTerminal,
|
||||||
} = useTerminal();
|
} = useTerminal()
|
||||||
|
|
||||||
const activeTerminal = terminals.find((t) => t.id === activeTerminalId);
|
const activeTerminal = terminals.find((t) => t.id === activeTerminalId)
|
||||||
|
|
||||||
// Effect to set the active terminal when a new one is created
|
// Effect to set the active terminal when a new one is created
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (terminals.length > 0 && !activeTerminalId) {
|
if (terminals.length > 0 && !activeTerminalId) {
|
||||||
setActiveTerminalId(terminals[terminals.length - 1].id);
|
setActiveTerminalId(terminals[terminals.length - 1].id)
|
||||||
}
|
}
|
||||||
}, [terminals, activeTerminalId, setActiveTerminalId]);
|
}, [terminals, activeTerminalId, setActiveTerminalId])
|
||||||
|
|
||||||
const handleCreateTerminal = () => {
|
const handleCreateTerminal = () => {
|
||||||
if (terminals.length >= 4) {
|
if (terminals.length >= 4) {
|
||||||
toast.error("You reached the maximum # of terminals.");
|
toast.error("You reached the maximum # of terminals.")
|
||||||
return;
|
return
|
||||||
|
}
|
||||||
|
createNewTerminal()
|
||||||
}
|
}
|
||||||
createNewTerminal();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -85,7 +84,7 @@ export default function Terminals() {
|
|||||||
? { ...term, terminal: t }
|
? { ...term, terminal: t }
|
||||||
: term
|
: term
|
||||||
)
|
)
|
||||||
);
|
)
|
||||||
}}
|
}}
|
||||||
visible={activeTerminalId === term.id}
|
visible={activeTerminalId === term.id}
|
||||||
/>
|
/>
|
||||||
@ -98,5 +97,5 @@ export default function Terminals() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
@ -1,14 +1,14 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Terminal } from "@xterm/xterm"
|
|
||||||
import { FitAddon } from "@xterm/addon-fit"
|
import { FitAddon } from "@xterm/addon-fit"
|
||||||
|
import { Terminal } from "@xterm/xterm"
|
||||||
import "./xterm.css"
|
import "./xterm.css"
|
||||||
|
|
||||||
import { ElementRef, useEffect, useRef, useState } from "react"
|
|
||||||
import { Socket } from "socket.io-client"
|
|
||||||
import { Loader2 } from "lucide-react"
|
|
||||||
import { debounce } from "@/lib/utils"
|
import { debounce } from "@/lib/utils"
|
||||||
|
import { Loader2 } from "lucide-react"
|
||||||
|
import { useTheme } from "next-themes"
|
||||||
|
import { ElementRef, useEffect, useRef } from "react"
|
||||||
|
import { Socket } from "socket.io-client"
|
||||||
export default function EditorTerminal({
|
export default function EditorTerminal({
|
||||||
socket,
|
socket,
|
||||||
id,
|
id,
|
||||||
@ -22,16 +22,121 @@ export default function EditorTerminal({
|
|||||||
setTerm: (term: Terminal) => void
|
setTerm: (term: Terminal) => void
|
||||||
visible: boolean
|
visible: boolean
|
||||||
}) {
|
}) {
|
||||||
const terminalRef = useRef<ElementRef<"div">>(null)
|
const { theme } = useTheme()
|
||||||
|
const terminalContainerRef = useRef<ElementRef<"div">>(null)
|
||||||
const fitAddonRef = useRef<FitAddon | null>(null)
|
const fitAddonRef = useRef<FitAddon | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!terminalRef.current) return
|
if (!terminalContainerRef.current) return
|
||||||
// console.log("new terminal", id, term ? "reusing" : "creating");
|
// console.log("new terminal", id, term ? "reusing" : "creating");
|
||||||
|
|
||||||
const terminal = new Terminal({
|
const terminal = new Terminal({
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
theme: {
|
theme: theme === "light" ? lightTheme : darkTheme,
|
||||||
|
fontFamily: "var(--font-geist-mono)",
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
letterSpacing: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
setTerm(terminal)
|
||||||
|
const dispose = () => {
|
||||||
|
terminal.dispose()
|
||||||
|
}
|
||||||
|
return dispose
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (term) {
|
||||||
|
term.options.theme = theme === "light" ? lightTheme : darkTheme
|
||||||
|
}
|
||||||
|
}, [theme])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!term) return
|
||||||
|
|
||||||
|
if (!terminalContainerRef.current) return
|
||||||
|
if (!fitAddonRef.current) {
|
||||||
|
const fitAddon = new FitAddon()
|
||||||
|
term.loadAddon(fitAddon)
|
||||||
|
term.open(terminalContainerRef.current)
|
||||||
|
fitAddon.fit()
|
||||||
|
fitAddonRef.current = fitAddon
|
||||||
|
}
|
||||||
|
|
||||||
|
const disposableOnData = term.onData((data) => {
|
||||||
|
socket.emit("terminalData", { id, data })
|
||||||
|
})
|
||||||
|
|
||||||
|
const disposableOnResize = term.onResize((dimensions) => {
|
||||||
|
fitAddonRef.current?.fit()
|
||||||
|
socket.emit("terminalResize", { dimensions })
|
||||||
|
})
|
||||||
|
const resizeObserver = new ResizeObserver(
|
||||||
|
debounce((entries) => {
|
||||||
|
if (!fitAddonRef.current || !terminalContainerRef.current) return
|
||||||
|
|
||||||
|
const entry = entries[0]
|
||||||
|
if (!entry) return
|
||||||
|
|
||||||
|
const { width, height } = entry.contentRect
|
||||||
|
|
||||||
|
// Only call fit if the size has actually changed
|
||||||
|
if (
|
||||||
|
width !== terminalContainerRef.current.offsetWidth ||
|
||||||
|
height !== terminalContainerRef.current.offsetHeight
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
fitAddonRef.current.fit()
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error during fit:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 50) // Debounce for 50ms
|
||||||
|
)
|
||||||
|
|
||||||
|
// start observing for resize
|
||||||
|
resizeObserver.observe(terminalContainerRef.current)
|
||||||
|
return () => {
|
||||||
|
disposableOnData.dispose()
|
||||||
|
disposableOnResize.dispose()
|
||||||
|
resizeObserver.disconnect()
|
||||||
|
}
|
||||||
|
}, [term, terminalContainerRef.current])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!term) return
|
||||||
|
const handleTerminalResponse = (response: { id: string; data: string }) => {
|
||||||
|
if (response.id === id) {
|
||||||
|
term.write(response.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
socket.on("terminalResponse", handleTerminalResponse)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.off("terminalResponse", handleTerminalResponse)
|
||||||
|
}
|
||||||
|
}, [term, id, socket])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
ref={terminalContainerRef}
|
||||||
|
style={{ display: visible ? "block" : "none" }}
|
||||||
|
className="w-full h-full text-left"
|
||||||
|
>
|
||||||
|
{term === null ? (
|
||||||
|
<div className="flex items-center text-muted-foreground p-2">
|
||||||
|
<Loader2 className="animate-spin mr-2 h-4 w-4" />
|
||||||
|
<span>Connecting to terminal...</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const lightTheme = {
|
||||||
foreground: "#2e3436",
|
foreground: "#2e3436",
|
||||||
background: "#ffffff",
|
background: "#ffffff",
|
||||||
black: "#2e3436",
|
black: "#2e3436",
|
||||||
@ -52,101 +157,34 @@ export default function EditorTerminal({
|
|||||||
brightWhite: "#eeeeec",
|
brightWhite: "#eeeeec",
|
||||||
cursor: "#2e3436",
|
cursor: "#2e3436",
|
||||||
cursorAccent: "#ffffff",
|
cursorAccent: "#ffffff",
|
||||||
selection: "rgba(52, 101, 164, 0.3)",
|
selectionBackground: "#3465a4",
|
||||||
},
|
selectionForeground: "#ffffff",
|
||||||
fontFamily: "var(--font-geist-mono)",
|
selectionInactiveBackground: "#264973",
|
||||||
fontSize: 14,
|
}
|
||||||
lineHeight: 1.5,
|
|
||||||
letterSpacing: 0,
|
// Dark Theme
|
||||||
})
|
const darkTheme = {
|
||||||
|
foreground: "#f8f8f2",
|
||||||
setTerm(terminal)
|
background: "#0a0a0a",
|
||||||
const dispose = () => {
|
black: "#21222c",
|
||||||
terminal.dispose()
|
brightBlack: "#6272a4",
|
||||||
}
|
red: "#ff5555",
|
||||||
return dispose
|
brightRed: "#ff6e6e",
|
||||||
}, [])
|
green: "#50fa7b",
|
||||||
|
brightGreen: "#69ff94",
|
||||||
useEffect(() => {
|
yellow: "#f1fa8c",
|
||||||
if (!term) return
|
brightYellow: "#ffffa5",
|
||||||
|
blue: "#bd93f9",
|
||||||
if (!terminalRef.current) return
|
brightBlue: "#d6acff",
|
||||||
if (!fitAddonRef.current) {
|
magenta: "#ff79c6",
|
||||||
const fitAddon = new FitAddon()
|
brightMagenta: "#ff92df",
|
||||||
term.loadAddon(fitAddon)
|
cyan: "#8be9fd",
|
||||||
term.open(terminalRef.current)
|
brightCyan: "#a4ffff",
|
||||||
fitAddon.fit()
|
white: "#f8f8f2",
|
||||||
fitAddonRef.current = fitAddon
|
brightWhite: "#ffffff",
|
||||||
}
|
cursor: "#f8f8f2",
|
||||||
|
cursorAccent: "#0a0a0a",
|
||||||
const disposableOnData = term.onData((data) => {
|
selectionBackground: "#264973",
|
||||||
socket.emit("terminalData", id, data)
|
selectionForeground: "#ffffff",
|
||||||
})
|
selectionInactiveBackground: "#1a3151",
|
||||||
|
|
||||||
const disposableOnResize = term.onResize((dimensions) => {
|
|
||||||
fitAddonRef.current?.fit()
|
|
||||||
socket.emit("terminalResize", dimensions)
|
|
||||||
})
|
|
||||||
const resizeObserver = new ResizeObserver(
|
|
||||||
debounce((entries) => {
|
|
||||||
if (!fitAddonRef.current || !terminalRef.current) return
|
|
||||||
|
|
||||||
const entry = entries[0]
|
|
||||||
if (!entry) return
|
|
||||||
|
|
||||||
const { width, height } = entry.contentRect
|
|
||||||
|
|
||||||
// Only call fit if the size has actually changed
|
|
||||||
if (
|
|
||||||
width !== terminalRef.current.offsetWidth ||
|
|
||||||
height !== terminalRef.current.offsetHeight
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
fitAddonRef.current.fit()
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error during fit:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 50) // Debounce for 50ms
|
|
||||||
)
|
|
||||||
|
|
||||||
// start observing for resize
|
|
||||||
resizeObserver.observe(terminalRef.current)
|
|
||||||
return () => {
|
|
||||||
disposableOnData.dispose()
|
|
||||||
disposableOnResize.dispose()
|
|
||||||
resizeObserver.disconnect()
|
|
||||||
}
|
|
||||||
}, [term, terminalRef.current])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!term) return
|
|
||||||
const handleTerminalResponse = (response: { id: string; data: string }) => {
|
|
||||||
if (response.id === id) {
|
|
||||||
term.write(response.data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
socket.on("terminalResponse", handleTerminalResponse)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
socket.off("terminalResponse", handleTerminalResponse)
|
|
||||||
}
|
|
||||||
}, [term, id, socket])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
ref={terminalRef}
|
|
||||||
style={{ display: visible ? "block" : "none" }}
|
|
||||||
className="w-full h-full text-left"
|
|
||||||
>
|
|
||||||
{term === null ? (
|
|
||||||
<div className="flex items-center text-muted-foreground p-2">
|
|
||||||
<Loader2 className="animate-spin mr-2 h-4 w-4" />
|
|
||||||
<span>Connecting to terminal...</span>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@
|
|||||||
* Default styles for xterm.js
|
* Default styles for xterm.js
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.xterm {
|
.xterm {
|
||||||
cursor: text;
|
cursor: text;
|
||||||
position: relative;
|
position: relative;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -80,7 +80,7 @@
|
|||||||
.xterm .composition-view {
|
.xterm .composition-view {
|
||||||
/* TODO: Composition position got messed up somewhere */
|
/* TODO: Composition position got messed up somewhere */
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #FFF;
|
color: #fff;
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -154,12 +154,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.xterm .xterm-accessibility-tree:not(.debug) *::selection {
|
.xterm .xterm-accessibility-tree:not(.debug) *::selection {
|
||||||
color: transparent;
|
color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xterm .xterm-accessibility-tree {
|
.xterm .xterm-accessibility-tree {
|
||||||
user-select: text;
|
user-select: text;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xterm .live-region {
|
.xterm .live-region {
|
||||||
@ -176,33 +176,55 @@ white-space: pre;
|
|||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xterm-underline-1 { text-decoration: underline; }
|
.xterm-underline-1 {
|
||||||
.xterm-underline-2 { text-decoration: double underline; }
|
text-decoration: underline;
|
||||||
.xterm-underline-3 { text-decoration: wavy underline; }
|
}
|
||||||
.xterm-underline-4 { text-decoration: dotted underline; }
|
.xterm-underline-2 {
|
||||||
.xterm-underline-5 { text-decoration: dashed underline; }
|
text-decoration: double underline;
|
||||||
|
}
|
||||||
|
.xterm-underline-3 {
|
||||||
|
text-decoration: wavy underline;
|
||||||
|
}
|
||||||
|
.xterm-underline-4 {
|
||||||
|
text-decoration: dotted underline;
|
||||||
|
}
|
||||||
|
.xterm-underline-5 {
|
||||||
|
text-decoration: dashed underline;
|
||||||
|
}
|
||||||
|
|
||||||
.xterm-overline {
|
.xterm-overline {
|
||||||
text-decoration: overline;
|
text-decoration: overline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
|
.xterm-overline.xterm-underline-1 {
|
||||||
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
|
text-decoration: overline underline;
|
||||||
.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
|
}
|
||||||
.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
|
.xterm-overline.xterm-underline-2 {
|
||||||
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
|
text-decoration: overline double underline;
|
||||||
|
}
|
||||||
|
.xterm-overline.xterm-underline-3 {
|
||||||
|
text-decoration: overline wavy underline;
|
||||||
|
}
|
||||||
|
.xterm-overline.xterm-underline-4 {
|
||||||
|
text-decoration: overline dotted underline;
|
||||||
|
}
|
||||||
|
.xterm-overline.xterm-underline-5 {
|
||||||
|
text-decoration: overline dashed underline;
|
||||||
|
}
|
||||||
|
|
||||||
.xterm-strikethrough {
|
.xterm-strikethrough {
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xterm-screen .xterm-decoration-container .xterm-decoration {
|
.xterm-screen .xterm-decoration-container .xterm-decoration {
|
||||||
z-index: 6;
|
z-index: 6;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
|
.xterm-screen
|
||||||
z-index: 7;
|
.xterm-decoration-container
|
||||||
|
.xterm-decoration.xterm-decoration-top-layer {
|
||||||
|
z-index: 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xterm-decoration-overview-ruler {
|
.xterm-decoration-overview-ruler {
|
||||||
|
@ -22,7 +22,7 @@ export default function Landing() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<Button variant="outline" size="icon" asChild>
|
<Button variant="outline" size="icon" asChild>
|
||||||
<a href="https://www.x.com/ishaandey_" target="_blank">
|
<a href="https://x.com/gitwitdev" target="_blank">
|
||||||
<svg
|
<svg
|
||||||
width="1200"
|
width="1200"
|
||||||
height="1227"
|
height="1227"
|
||||||
@ -54,7 +54,7 @@ export default function Landing() {
|
|||||||
<CustomButton>Go To App</CustomButton>
|
<CustomButton>Go To App</CustomButton>
|
||||||
</Link>
|
</Link>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/ishaan1013/sandbox"
|
href="https://github.com/jamesmurdza/sandbox"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="group h-9 px-4 py-2 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
className="group h-9 px-4 py-2 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
||||||
>
|
>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React from "react"
|
||||||
|
|
||||||
const LoadingDots: React.FC = () => {
|
const LoadingDots: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
@ -15,18 +15,29 @@ const LoadingDots: React.FC = () => {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
animation: showHideDot 1.5s ease-in-out infinite;
|
animation: showHideDot 1.5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
.dot:nth-child(1) { animation-delay: 0s; }
|
.dot:nth-child(1) {
|
||||||
.dot:nth-child(2) { animation-delay: 0.5s; }
|
animation-delay: 0s;
|
||||||
.dot:nth-child(3) { animation-delay: 1s; }
|
}
|
||||||
|
.dot:nth-child(2) {
|
||||||
|
animation-delay: 0.5s;
|
||||||
|
}
|
||||||
|
.dot:nth-child(3) {
|
||||||
|
animation-delay: 1s;
|
||||||
|
}
|
||||||
@keyframes showHideDot {
|
@keyframes showHideDot {
|
||||||
0% { opacity: 0; }
|
0% {
|
||||||
50% { opacity: 1; }
|
opacity: 0;
|
||||||
100% { opacity: 0; }
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</span>
|
</span>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default LoadingDots;
|
|
||||||
|
|
||||||
|
export default LoadingDots
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { buttonVariants } from "@/components/ui/button"
|
import { buttonVariants } from "@/components/ui/button"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const AlertDialog = AlertDialogPrimitive.Root
|
const AlertDialog = AlertDialogPrimitive.Root
|
||||||
|
|
||||||
@ -128,14 +128,14 @@ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogPortal,
|
|
||||||
AlertDialogOverlay,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogPortal,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
@ -73,4 +73,4 @@ const CardFooter = React.forwardRef<
|
|||||||
))
|
))
|
||||||
CardFooter.displayName = "CardFooter"
|
CardFooter.displayName = "CardFooter"
|
||||||
|
|
||||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
DotFilledIcon,
|
DotFilledIcon,
|
||||||
} from "@radix-ui/react-icons"
|
} from "@radix-ui/react-icons"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@ -187,18 +187,18 @@ ContextMenuShortcut.displayName = "ContextMenuShortcut"
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuTrigger,
|
|
||||||
ContextMenuContent,
|
|
||||||
ContextMenuItem,
|
|
||||||
ContextMenuCheckboxItem,
|
ContextMenuCheckboxItem,
|
||||||
ContextMenuRadioItem,
|
ContextMenuContent,
|
||||||
|
ContextMenuGroup,
|
||||||
|
ContextMenuItem,
|
||||||
ContextMenuLabel,
|
ContextMenuLabel,
|
||||||
|
ContextMenuPortal,
|
||||||
|
ContextMenuRadioGroup,
|
||||||
|
ContextMenuRadioItem,
|
||||||
ContextMenuSeparator,
|
ContextMenuSeparator,
|
||||||
ContextMenuShortcut,
|
ContextMenuShortcut,
|
||||||
ContextMenuGroup,
|
|
||||||
ContextMenuPortal,
|
|
||||||
ContextMenuSub,
|
ContextMenuSub,
|
||||||
ContextMenuSubContent,
|
ContextMenuSubContent,
|
||||||
ContextMenuSubTrigger,
|
ContextMenuSubTrigger,
|
||||||
ContextMenuRadioGroup,
|
ContextMenuTrigger,
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import * as React from "react";
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
import { Cross2Icon } from "@radix-ui/react-icons"
|
||||||
import { Cross2Icon } from "@radix-ui/react-icons";
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Dialog = DialogPrimitive.Root;
|
const Dialog = DialogPrimitive.Root
|
||||||
|
|
||||||
const DialogTrigger = DialogPrimitive.Trigger;
|
const DialogTrigger = DialogPrimitive.Trigger
|
||||||
|
|
||||||
const DialogPortal = DialogPrimitive.Portal;
|
const DialogPortal = DialogPrimitive.Portal
|
||||||
|
|
||||||
const DialogClose = DialogPrimitive.Close;
|
const DialogClose = DialogPrimitive.Close
|
||||||
|
|
||||||
const DialogOverlay = React.forwardRef<
|
const DialogOverlay = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
@ -26,8 +26,8 @@ const DialogOverlay = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
))
|
||||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
const DialogContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
@ -50,8 +50,8 @@ const DialogContent = React.forwardRef<
|
|||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
));
|
))
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||||
|
|
||||||
const DialogContentNoClose = React.forwardRef<
|
const DialogContentNoClose = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
@ -70,9 +70,9 @@ const DialogContentNoClose = React.forwardRef<
|
|||||||
{children}
|
{children}
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
));
|
))
|
||||||
DialogContentNoClose.displayName =
|
DialogContentNoClose.displayName =
|
||||||
DialogPrimitive.Content.displayName + "NoClose";
|
DialogPrimitive.Content.displayName + "NoClose"
|
||||||
|
|
||||||
const DialogHeader = ({
|
const DialogHeader = ({
|
||||||
className,
|
className,
|
||||||
@ -85,8 +85,8 @@ const DialogHeader = ({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
DialogHeader.displayName = "DialogHeader";
|
DialogHeader.displayName = "DialogHeader"
|
||||||
|
|
||||||
const DialogFooter = ({
|
const DialogFooter = ({
|
||||||
className,
|
className,
|
||||||
@ -99,8 +99,8 @@ const DialogFooter = ({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
DialogFooter.displayName = "DialogFooter";
|
DialogFooter.displayName = "DialogFooter"
|
||||||
|
|
||||||
const DialogTitle = React.forwardRef<
|
const DialogTitle = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
@ -114,8 +114,8 @@ const DialogTitle = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
))
|
||||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||||
|
|
||||||
const DialogDescription = React.forwardRef<
|
const DialogDescription = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
@ -126,19 +126,19 @@ const DialogDescription = React.forwardRef<
|
|||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
))
|
||||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogPortal,
|
|
||||||
DialogOverlay,
|
|
||||||
DialogTrigger,
|
|
||||||
DialogClose,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogContentNoClose,
|
DialogContentNoClose,
|
||||||
DialogHeader,
|
|
||||||
DialogFooter,
|
|
||||||
DialogTitle,
|
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
};
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogPortal,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
DotFilledIcon,
|
DotFilledIcon,
|
||||||
} from "@radix-ui/react-icons"
|
} from "@radix-ui/react-icons"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@ -188,18 +188,18 @@ DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
DropdownMenuRadioItem,
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuSub,
|
DropdownMenuSub,
|
||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuRadioGroup,
|
DropdownMenuTrigger,
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import * as React from "react"
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
ControllerProps,
|
ControllerProps,
|
||||||
@ -10,8 +10,8 @@ import {
|
|||||||
useFormContext,
|
useFormContext,
|
||||||
} from "react-hook-form"
|
} from "react-hook-form"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Form = FormProvider
|
const Form = FormProvider
|
||||||
|
|
||||||
@ -165,12 +165,12 @@ const FormMessage = React.forwardRef<
|
|||||||
FormMessage.displayName = "FormMessage"
|
FormMessage.displayName = "FormMessage"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useFormField,
|
|
||||||
Form,
|
Form,
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
FormDescription,
|
||||||
FormMessage,
|
|
||||||
FormField,
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
useFormField,
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@ -30,4 +30,4 @@ const PopoverContent = React.forwardRef<
|
|||||||
))
|
))
|
||||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||||
|
|
||||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger }
|
||||||
|
@ -42,4 +42,4 @@ const ResizableHandle = ({
|
|||||||
</ResizablePrimitive.PanelResizeHandle>
|
</ResizablePrimitive.PanelResizeHandle>
|
||||||
)
|
)
|
||||||
|
|
||||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
export { ResizableHandle, ResizablePanel, ResizablePanelGroup }
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import {
|
import {
|
||||||
CaretSortIcon,
|
CaretSortIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
@ -8,6 +7,7 @@ import {
|
|||||||
ChevronUpIcon,
|
ChevronUpIcon,
|
||||||
} from "@radix-ui/react-icons"
|
} from "@radix-ui/react-icons"
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@ -152,13 +152,13 @@ SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
Select,
|
Select,
|
||||||
SelectGroup,
|
|
||||||
SelectValue,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectLabel,
|
SelectGroup,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectSeparator,
|
SelectLabel,
|
||||||
SelectScrollUpButton,
|
|
||||||
SelectScrollDownButton,
|
SelectScrollDownButton,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { Loader2, X } from "lucide-react";
|
import { Loader2, X } from "lucide-react"
|
||||||
import { Button } from "./button";
|
import { MouseEventHandler } from "react"
|
||||||
import { MouseEvent, MouseEventHandler, useEffect } from "react";
|
import { Button } from "./button"
|
||||||
|
|
||||||
export default function Tab({
|
export default function Tab({
|
||||||
children,
|
children,
|
||||||
@ -13,13 +13,13 @@ export default function Tab({
|
|||||||
onClose,
|
onClose,
|
||||||
closing = false,
|
closing = false,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
creating?: boolean;
|
creating?: boolean
|
||||||
saved?: boolean;
|
saved?: boolean
|
||||||
selected?: boolean;
|
selected?: boolean
|
||||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
onClick?: MouseEventHandler<HTMLButtonElement>
|
||||||
onClose?: () => void;
|
onClose?: () => void
|
||||||
closing?: boolean;
|
closing?: boolean
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@ -28,7 +28,7 @@ export default function Tab({
|
|||||||
variant={"secondary"}
|
variant={"secondary"}
|
||||||
className={`font-normal select-none ${
|
className={`font-normal select-none ${
|
||||||
selected
|
selected
|
||||||
? "bg-neutral-700 hover:bg-neutral-600 text-foreground"
|
? "bg-muted-foreground/50 hover:bg-muted-foreground/40 text-foreground"
|
||||||
: "text-muted-foreground"
|
: "text-muted-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -37,9 +37,9 @@ export default function Tab({
|
|||||||
onClick={
|
onClick={
|
||||||
onClose && !closing
|
onClose && !closing
|
||||||
? (e) => {
|
? (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation()
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
onClose();
|
onClose()
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
@ -57,5 +57,5 @@ export default function Tab({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -110,11 +110,11 @@ TableCaption.displayName = "TableCaption"
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
Table,
|
Table,
|
||||||
TableHeader,
|
|
||||||
TableBody,
|
TableBody,
|
||||||
|
TableCaption,
|
||||||
|
TableCell,
|
||||||
TableFooter,
|
TableFooter,
|
||||||
TableHead,
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
TableCell,
|
|
||||||
TableCaption,
|
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,22 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu"
|
||||||
import { User } from "@/lib/types";
|
import { User } from "@/lib/types"
|
||||||
import { useClerk } from "@clerk/nextjs";
|
import { useClerk } from "@clerk/nextjs"
|
||||||
import { LogOut, Pencil, Sparkles } from "lucide-react";
|
import { LogOut, Sparkles } from "lucide-react"
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation"
|
||||||
|
|
||||||
export default function UserButton({ userData }: { userData: User }) {
|
export default function UserButton({ userData }: { userData: User }) {
|
||||||
if (!userData) return null;
|
if (!userData) return null
|
||||||
|
|
||||||
const { signOut } = useClerk();
|
const { signOut } = useClerk()
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@ -68,5 +67,5 @@ export default function UserButton({ userData }: { userData: User }) {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,34 +1,44 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import React, { createContext, useContext, useState, useRef } from 'react';
|
import React, { createContext, useContext, useRef, useState } from "react"
|
||||||
import { ImperativePanelHandle } from "react-resizable-panels";
|
import { ImperativePanelHandle } from "react-resizable-panels"
|
||||||
|
|
||||||
interface PreviewContextType {
|
interface PreviewContextType {
|
||||||
isPreviewCollapsed: boolean;
|
isPreviewCollapsed: boolean
|
||||||
setIsPreviewCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsPreviewCollapsed: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
previewURL: string;
|
previewURL: string
|
||||||
setPreviewURL: React.Dispatch<React.SetStateAction<string>>;
|
setPreviewURL: React.Dispatch<React.SetStateAction<string>>
|
||||||
previewPanelRef: React.RefObject<ImperativePanelHandle>;
|
previewPanelRef: React.RefObject<ImperativePanelHandle>
|
||||||
}
|
}
|
||||||
|
|
||||||
const PreviewContext = createContext<PreviewContextType | undefined>(undefined);
|
const PreviewContext = createContext<PreviewContextType | undefined>(undefined)
|
||||||
|
|
||||||
export const PreviewProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const PreviewProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true);
|
children,
|
||||||
const [previewURL, setPreviewURL] = useState<string>("");
|
}) => {
|
||||||
const previewPanelRef = useRef<ImperativePanelHandle>(null);
|
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
|
||||||
|
const [previewURL, setPreviewURL] = useState<string>("")
|
||||||
|
const previewPanelRef = useRef<ImperativePanelHandle>(null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PreviewContext.Provider value={{ isPreviewCollapsed, setIsPreviewCollapsed, previewURL, setPreviewURL, previewPanelRef }}>
|
<PreviewContext.Provider
|
||||||
|
value={{
|
||||||
|
isPreviewCollapsed,
|
||||||
|
setIsPreviewCollapsed,
|
||||||
|
previewURL,
|
||||||
|
setPreviewURL,
|
||||||
|
previewPanelRef,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</PreviewContext.Provider>
|
</PreviewContext.Provider>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export const usePreview = () => {
|
export const usePreview = () => {
|
||||||
const context = useContext(PreviewContext);
|
const context = useContext(PreviewContext)
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
throw new Error('usePreview must be used within a PreviewProvider');
|
throw new Error("usePreview must be used within a PreviewProvider")
|
||||||
}
|
}
|
||||||
return context;
|
return context
|
||||||
};
|
}
|
||||||
|
@ -1,63 +1,65 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
import React, { createContext, useContext, useEffect, useState } from "react"
|
||||||
import { io, Socket } from 'socket.io-client';
|
import { io, Socket } from "socket.io-client"
|
||||||
|
|
||||||
interface SocketContextType {
|
interface SocketContextType {
|
||||||
socket: Socket | null;
|
socket: Socket | null
|
||||||
setUserAndSandboxId: (userId: string, sandboxId: string) => void;
|
setUserAndSandboxId: (userId: string, sandboxId: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const SocketContext = createContext<SocketContextType | undefined>(undefined);
|
const SocketContext = createContext<SocketContextType | undefined>(undefined)
|
||||||
|
|
||||||
export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
const [socket, setSocket] = useState<Socket | null>(null);
|
children,
|
||||||
const [userId, setUserId] = useState<string | null>(null);
|
}) => {
|
||||||
const [sandboxId, setSandboxId] = useState<string | null>(null);
|
const [socket, setSocket] = useState<Socket | null>(null)
|
||||||
|
const [userId, setUserId] = useState<string | null>(null)
|
||||||
|
const [sandboxId, setSandboxId] = useState<string | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userId && sandboxId) {
|
if (userId && sandboxId) {
|
||||||
console.log("Initializing socket connection...");
|
console.log("Initializing socket connection...")
|
||||||
const newSocket = io(`${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userId}&sandboxId=${sandboxId}`);
|
const newSocket = io(
|
||||||
console.log("Socket instance:", newSocket);
|
`${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userId}&sandboxId=${sandboxId}`
|
||||||
setSocket(newSocket);
|
)
|
||||||
|
console.log("Socket instance:", newSocket)
|
||||||
|
setSocket(newSocket)
|
||||||
|
|
||||||
newSocket.on('connect', () => {
|
newSocket.on("connect", () => {
|
||||||
console.log("Socket connected:", newSocket.id);
|
console.log("Socket connected:", newSocket.id)
|
||||||
});
|
})
|
||||||
|
|
||||||
newSocket.on('disconnect', () => {
|
newSocket.on("disconnect", () => {
|
||||||
console.log("Socket disconnected");
|
console.log("Socket disconnected")
|
||||||
});
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
console.log("Disconnecting socket...");
|
console.log("Disconnecting socket...")
|
||||||
newSocket.disconnect();
|
newSocket.disconnect()
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}, [userId, sandboxId]);
|
}
|
||||||
|
}, [userId, sandboxId])
|
||||||
|
|
||||||
const setUserAndSandboxId = (newUserId: string, newSandboxId: string) => {
|
const setUserAndSandboxId = (newUserId: string, newSandboxId: string) => {
|
||||||
setUserId(newUserId);
|
setUserId(newUserId)
|
||||||
setSandboxId(newSandboxId);
|
setSandboxId(newSandboxId)
|
||||||
};
|
}
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
socket,
|
socket,
|
||||||
setUserAndSandboxId,
|
setUserAndSandboxId,
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SocketContext.Provider value={ value }>
|
<SocketContext.Provider value={value}>{children}</SocketContext.Provider>
|
||||||
{children}
|
)
|
||||||
</SocketContext.Provider>
|
}
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useSocket = (): SocketContextType => {
|
export const useSocket = (): SocketContextType => {
|
||||||
const context = useContext(SocketContext);
|
const context = useContext(SocketContext)
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('useSocket must be used within a SocketProvider');
|
throw new Error("useSocket must be used within a SocketProvider")
|
||||||
}
|
}
|
||||||
return context;
|
return context
|
||||||
};
|
}
|
||||||
|
@ -1,33 +1,44 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import React, { createContext, useContext, useState } from 'react';
|
import { useSocket } from "@/context/SocketContext"
|
||||||
import { Terminal } from '@xterm/xterm';
|
import {
|
||||||
import { createTerminal as createTerminalHelper, closeTerminal as closeTerminalHelper } from '@/lib/terminal';
|
closeTerminal as closeTerminalHelper,
|
||||||
import { useSocket } from '@/context/SocketContext';
|
createTerminal as createTerminalHelper,
|
||||||
|
} from "@/lib/terminal"
|
||||||
|
import { Terminal } from "@xterm/xterm"
|
||||||
|
import React, { createContext, useContext, useState } from "react"
|
||||||
|
|
||||||
interface TerminalContextType {
|
interface TerminalContextType {
|
||||||
terminals: { id: string; terminal: Terminal | null }[];
|
terminals: { id: string; terminal: Terminal | null }[]
|
||||||
setTerminals: React.Dispatch<React.SetStateAction<{ id: string; terminal: Terminal | null }[]>>;
|
setTerminals: React.Dispatch<
|
||||||
activeTerminalId: string;
|
React.SetStateAction<{ id: string; terminal: Terminal | null }[]>
|
||||||
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>;
|
>
|
||||||
creatingTerminal: boolean;
|
activeTerminalId: string
|
||||||
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>;
|
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>
|
||||||
createNewTerminal: (command?: string) => Promise<void>;
|
creatingTerminal: boolean
|
||||||
closeTerminal: (id: string) => void;
|
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
deploy: (callback: () => void) => void;
|
createNewTerminal: (command?: string) => Promise<void>
|
||||||
|
closeTerminal: (id: string) => void
|
||||||
|
deploy: (callback: () => void) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const TerminalContext = createContext<TerminalContextType | undefined>(undefined);
|
const TerminalContext = createContext<TerminalContextType | undefined>(
|
||||||
|
undefined
|
||||||
|
)
|
||||||
|
|
||||||
export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
const { socket } = useSocket();
|
children,
|
||||||
const [terminals, setTerminals] = useState<{ id: string; terminal: Terminal | null }[]>([]);
|
}) => {
|
||||||
const [activeTerminalId, setActiveTerminalId] = useState<string>('');
|
const { socket } = useSocket()
|
||||||
const [creatingTerminal, setCreatingTerminal] = useState<boolean>(false);
|
const [terminals, setTerminals] = useState<
|
||||||
|
{ id: string; terminal: Terminal | null }[]
|
||||||
|
>([])
|
||||||
|
const [activeTerminalId, setActiveTerminalId] = useState<string>("")
|
||||||
|
const [creatingTerminal, setCreatingTerminal] = useState<boolean>(false)
|
||||||
|
|
||||||
const createNewTerminal = async (command?: string): Promise<void> => {
|
const createNewTerminal = async (command?: string): Promise<void> => {
|
||||||
if (!socket) return;
|
if (!socket) return
|
||||||
setCreatingTerminal(true);
|
setCreatingTerminal(true)
|
||||||
try {
|
try {
|
||||||
createTerminalHelper({
|
createTerminalHelper({
|
||||||
setTerminals,
|
setTerminals,
|
||||||
@ -35,36 +46,36 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil
|
|||||||
setCreatingTerminal,
|
setCreatingTerminal,
|
||||||
command,
|
command,
|
||||||
socket,
|
socket,
|
||||||
});
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating terminal:", error);
|
console.error("Error creating terminal:", error)
|
||||||
} finally {
|
} finally {
|
||||||
setCreatingTerminal(false);
|
setCreatingTerminal(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const closeTerminal = (id: string) => {
|
const closeTerminal = (id: string) => {
|
||||||
if (!socket) return;
|
if (!socket) return
|
||||||
const terminalToClose = terminals.find(term => term.id === id);
|
const terminalToClose = terminals.find((term) => term.id === id)
|
||||||
if (terminalToClose) {
|
if (terminalToClose) {
|
||||||
closeTerminalHelper({
|
closeTerminalHelper({
|
||||||
term: terminalToClose,
|
term: terminalToClose,
|
||||||
terminals,
|
terminals,
|
||||||
setTerminals,
|
setTerminals,
|
||||||
setActiveTerminalId,
|
setActiveTerminalId,
|
||||||
setClosingTerminal: () => {},
|
setClosingTerminal: () => { },
|
||||||
socket,
|
socket,
|
||||||
activeTerminalId,
|
activeTerminalId,
|
||||||
});
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const deploy = (callback: () => void) => {
|
const deploy = (callback: () => void) => {
|
||||||
if (!socket) console.error("Couldn't deploy: No socket");
|
if (!socket) console.error("Couldn't deploy: No socket")
|
||||||
console.log("Deploying...")
|
console.log("Deploying...")
|
||||||
socket?.emit("deploy", () => {
|
socket?.emit("deploy", {}, () => {
|
||||||
callback();
|
callback()
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
@ -76,20 +87,20 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil
|
|||||||
setCreatingTerminal,
|
setCreatingTerminal,
|
||||||
createNewTerminal,
|
createNewTerminal,
|
||||||
closeTerminal,
|
closeTerminal,
|
||||||
deploy
|
deploy,
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TerminalContext.Provider value={value}>
|
<TerminalContext.Provider value={value}>
|
||||||
{children}
|
{children}
|
||||||
</TerminalContext.Provider>
|
</TerminalContext.Provider>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export const useTerminal = (): TerminalContextType => {
|
export const useTerminal = (): TerminalContextType => {
|
||||||
const context = useContext(TerminalContext);
|
const context = useContext(TerminalContext)
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('useTerminal must be used within a TerminalProvider');
|
throw new Error("useTerminal must be used within a TerminalProvider")
|
||||||
}
|
}
|
||||||
return context;
|
return context
|
||||||
};
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
// Helper functions for terminal instances
|
// Helper functions for terminal instances
|
||||||
|
|
||||||
import { createId } from "@paralleldrive/cuid2";
|
import { createId } from "@paralleldrive/cuid2"
|
||||||
import { Terminal } from "@xterm/xterm";
|
import { Terminal } from "@xterm/xterm"
|
||||||
import { Socket } from "socket.io-client";
|
import { Socket } from "socket.io-client"
|
||||||
|
|
||||||
export const createTerminal = ({
|
export const createTerminal = ({
|
||||||
setTerminals,
|
setTerminals,
|
||||||
@ -11,30 +11,33 @@ export const createTerminal = ({
|
|||||||
command,
|
command,
|
||||||
socket,
|
socket,
|
||||||
}: {
|
}: {
|
||||||
setTerminals: React.Dispatch<React.SetStateAction<{
|
setTerminals: React.Dispatch<
|
||||||
id: string;
|
React.SetStateAction<
|
||||||
terminal: Terminal | null;
|
{
|
||||||
}[]>>;
|
id: string
|
||||||
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>;
|
terminal: Terminal | null
|
||||||
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>;
|
}[]
|
||||||
command?: string;
|
>
|
||||||
socket: Socket;
|
>
|
||||||
|
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>
|
||||||
|
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
command?: string
|
||||||
|
socket: Socket
|
||||||
}) => {
|
}) => {
|
||||||
setCreatingTerminal(true);
|
setCreatingTerminal(true)
|
||||||
const id = createId();
|
const id = createId()
|
||||||
console.log("creating terminal, id:", id);
|
console.log("creating terminal, id:", id)
|
||||||
|
|
||||||
setTerminals((prev) => [...prev, { id, terminal: null }]);
|
setTerminals((prev) => [...prev, { id, terminal: null }])
|
||||||
setActiveTerminalId(id);
|
setActiveTerminalId(id)
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
socket.emit("createTerminal", id, () => {
|
socket.emit("createTerminal", { id }, () => {
|
||||||
setCreatingTerminal(false);
|
setCreatingTerminal(false)
|
||||||
if (command) socket.emit("terminalData", id, command + "\n");
|
if (command) socket.emit("terminalData", { id, data: command + "\n" })
|
||||||
});
|
})
|
||||||
}, 1000);
|
}, 1000)
|
||||||
};
|
}
|
||||||
|
|
||||||
export const closeTerminal = ({
|
export const closeTerminal = ({
|
||||||
term,
|
term,
|
||||||
@ -44,32 +47,36 @@ export const closeTerminal = ({
|
|||||||
setClosingTerminal,
|
setClosingTerminal,
|
||||||
socket,
|
socket,
|
||||||
activeTerminalId,
|
activeTerminalId,
|
||||||
} : {
|
}: {
|
||||||
term: {
|
term: {
|
||||||
id: string;
|
id: string
|
||||||
terminal: Terminal | null
|
terminal: Terminal | null
|
||||||
}
|
}
|
||||||
terminals: {
|
terminals: {
|
||||||
id: string;
|
id: string
|
||||||
terminal: Terminal | null
|
terminal: Terminal | null
|
||||||
}[]
|
}[]
|
||||||
setTerminals: React.Dispatch<React.SetStateAction<{
|
setTerminals: React.Dispatch<
|
||||||
id: string;
|
React.SetStateAction<
|
||||||
|
{
|
||||||
|
id: string
|
||||||
terminal: Terminal | null
|
terminal: Terminal | null
|
||||||
}[]>>
|
}[]
|
||||||
|
>
|
||||||
|
>
|
||||||
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>
|
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>
|
||||||
setClosingTerminal: React.Dispatch<React.SetStateAction<string>>
|
setClosingTerminal: React.Dispatch<React.SetStateAction<string>>
|
||||||
socket: Socket
|
socket: Socket
|
||||||
activeTerminalId: string
|
activeTerminalId: string
|
||||||
}) => {
|
}) => {
|
||||||
const numTerminals = terminals.length;
|
const numTerminals = terminals.length
|
||||||
const index = terminals.findIndex((t) => t.id === term.id);
|
const index = terminals.findIndex((t) => t.id === term.id)
|
||||||
if (index === -1) return;
|
if (index === -1) return
|
||||||
|
|
||||||
setClosingTerminal(term.id);
|
setClosingTerminal(term.id)
|
||||||
|
|
||||||
socket.emit("closeTerminal", term.id, () => {
|
socket.emit("closeTerminal", { id: term.id }, () => {
|
||||||
setClosingTerminal("");
|
setClosingTerminal("")
|
||||||
|
|
||||||
const nextId =
|
const nextId =
|
||||||
activeTerminalId === term.id
|
activeTerminalId === term.id
|
||||||
@ -78,17 +85,17 @@ export const closeTerminal = ({
|
|||||||
: index < numTerminals - 1
|
: index < numTerminals - 1
|
||||||
? terminals[index + 1].id
|
? terminals[index + 1].id
|
||||||
: terminals[index - 1].id
|
: terminals[index - 1].id
|
||||||
: activeTerminalId;
|
: activeTerminalId
|
||||||
|
|
||||||
setTerminals((prev) => prev.filter((t) => t.id !== term.id));
|
setTerminals((prev) => prev.filter((t) => t.id !== term.id))
|
||||||
|
|
||||||
if (!nextId) {
|
if (!nextId) {
|
||||||
setActiveTerminalId("");
|
setActiveTerminalId("")
|
||||||
} else {
|
} else {
|
||||||
const nextTerminal = terminals.find((t) => t.id === nextId);
|
const nextTerminal = terminals.find((t) => t.id === nextId)
|
||||||
if (nextTerminal) {
|
if (nextTerminal) {
|
||||||
setActiveTerminalId(nextTerminal.id);
|
setActiveTerminalId(nextTerminal.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
@ -73,7 +73,11 @@ function mapModule(module: string): monaco.languages.typescript.ModuleKind {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapJSX(jsx: string): monaco.languages.typescript.JsxEmit {
|
function mapJSX(jsx: string | undefined): monaco.languages.typescript.JsxEmit {
|
||||||
|
if (!jsx || typeof jsx !== 'string') {
|
||||||
|
return monaco.languages.typescript.JsxEmit.React // Default value
|
||||||
|
}
|
||||||
|
|
||||||
const jsxMap: { [key: string]: monaco.languages.typescript.JsxEmit } = {
|
const jsxMap: { [key: string]: monaco.languages.typescript.JsxEmit } = {
|
||||||
preserve: monaco.languages.typescript.JsxEmit.Preserve,
|
preserve: monaco.languages.typescript.JsxEmit.Preserve,
|
||||||
react: monaco.languages.typescript.JsxEmit.React,
|
react: monaco.languages.typescript.JsxEmit.React,
|
||||||
|
@ -1,65 +1,65 @@
|
|||||||
// DB Types
|
// DB Types
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
id: string;
|
id: string
|
||||||
name: string;
|
name: string
|
||||||
email: string;
|
email: string
|
||||||
generations: number;
|
generations: number
|
||||||
sandbox: Sandbox[];
|
sandbox: Sandbox[]
|
||||||
usersToSandboxes: UsersToSandboxes[];
|
usersToSandboxes: UsersToSandboxes[]
|
||||||
};
|
}
|
||||||
|
|
||||||
export type Sandbox = {
|
export type Sandbox = {
|
||||||
id: string;
|
id: string
|
||||||
name: string;
|
name: string
|
||||||
type: string;
|
type: string
|
||||||
visibility: "public" | "private";
|
visibility: "public" | "private"
|
||||||
createdAt: Date;
|
createdAt: Date
|
||||||
userId: string;
|
userId: string
|
||||||
usersToSandboxes: UsersToSandboxes[];
|
usersToSandboxes: UsersToSandboxes[]
|
||||||
};
|
}
|
||||||
|
|
||||||
export type UsersToSandboxes = {
|
export type UsersToSandboxes = {
|
||||||
userId: string;
|
userId: string
|
||||||
sandboxId: string;
|
sandboxId: string
|
||||||
sharedOn: Date;
|
sharedOn: Date
|
||||||
};
|
}
|
||||||
|
|
||||||
export type R2Files = {
|
export type R2Files = {
|
||||||
objects: R2FileData[];
|
objects: R2FileData[]
|
||||||
truncated: boolean;
|
truncated: boolean
|
||||||
delimitedPrefixes: any[];
|
delimitedPrefixes: any[]
|
||||||
};
|
}
|
||||||
|
|
||||||
export type R2FileData = {
|
export type R2FileData = {
|
||||||
storageClass: string;
|
storageClass: string
|
||||||
uploaded: string;
|
uploaded: string
|
||||||
checksums: any;
|
checksums: any
|
||||||
httpEtag: string;
|
httpEtag: string
|
||||||
etag: string;
|
etag: string
|
||||||
size: number;
|
size: number
|
||||||
version: string;
|
version: string
|
||||||
key: string;
|
key: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type TFolder = {
|
export type TFolder = {
|
||||||
id: string;
|
id: string
|
||||||
type: "folder";
|
type: "folder"
|
||||||
name: string;
|
name: string
|
||||||
children: (TFile | TFolder)[];
|
children: (TFile | TFolder)[]
|
||||||
};
|
}
|
||||||
|
|
||||||
export type TFile = {
|
export type TFile = {
|
||||||
id: string;
|
id: string
|
||||||
type: "file";
|
type: "file"
|
||||||
name: string;
|
name: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type TTab = TFile & {
|
export type TTab = TFile & {
|
||||||
saved: boolean;
|
saved: boolean
|
||||||
};
|
}
|
||||||
|
|
||||||
export type TFileData = {
|
export type TFileData = {
|
||||||
id: string;
|
id: string
|
||||||
data: string;
|
data: string
|
||||||
};
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { type ClassValue, clsx } from "clsx"
|
import { type ClassValue, clsx } from "clsx"
|
||||||
// import { toast } from "sonner"
|
// import { toast } from "sonner"
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge"
|
||||||
import { Sandbox, TFile, TFolder } from "./types"
|
|
||||||
import fileExtToLang from "./file-extension-to-language.json"
|
import fileExtToLang from "./file-extension-to-language.json"
|
||||||
|
import { Sandbox, TFile, TFolder } from "./types"
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
|
6
frontend/package-lock.json
generated
6
frontend/package-lock.json
generated
@ -39,6 +39,7 @@
|
|||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"embla-carousel": "^8.3.0",
|
||||||
"embla-carousel-react": "^8.3.0",
|
"embla-carousel-react": "^8.3.0",
|
||||||
"embla-carousel-wheel-gestures": "^8.0.1",
|
"embla-carousel-wheel-gestures": "^8.0.1",
|
||||||
"framer-motion": "^11.2.3",
|
"framer-motion": "^11.2.3",
|
||||||
@ -3533,12 +3534,14 @@
|
|||||||
"node_modules/embla-carousel": {
|
"node_modules/embla-carousel": {
|
||||||
"version": "8.3.0",
|
"version": "8.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.3.0.tgz",
|
||||||
"integrity": "sha512-Ve8dhI4w28qBqR8J+aMtv7rLK89r1ZA5HocwFz6uMB/i5EiC7bGI7y+AM80yAVUJw3qqaZYK7clmZMUR8kM3UA=="
|
"integrity": "sha512-Ve8dhI4w28qBqR8J+aMtv7rLK89r1ZA5HocwFz6uMB/i5EiC7bGI7y+AM80yAVUJw3qqaZYK7clmZMUR8kM3UA==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/embla-carousel-react": {
|
"node_modules/embla-carousel-react": {
|
||||||
"version": "8.3.0",
|
"version": "8.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.3.0.tgz",
|
||||||
"integrity": "sha512-P1FlinFDcIvggcErRjNuVqnUR8anyo8vLMIH8Rthgofw7Nj8qTguCa2QjFAbzxAUTQTPNNjNL7yt0BGGinVdFw==",
|
"integrity": "sha512-P1FlinFDcIvggcErRjNuVqnUR8anyo8vLMIH8Rthgofw7Nj8qTguCa2QjFAbzxAUTQTPNNjNL7yt0BGGinVdFw==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"embla-carousel": "8.3.0",
|
"embla-carousel": "8.3.0",
|
||||||
"embla-carousel-reactive-utils": "8.3.0"
|
"embla-carousel-reactive-utils": "8.3.0"
|
||||||
@ -3559,6 +3562,7 @@
|
|||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/embla-carousel-wheel-gestures/-/embla-carousel-wheel-gestures-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/embla-carousel-wheel-gestures/-/embla-carousel-wheel-gestures-8.0.1.tgz",
|
||||||
"integrity": "sha512-LMAnruDqDmsjL6UoQD65aLotpmfO49Fsr3H0bMi7I+BH6jbv9OJiE61kN56daKsVtCQEt0SU1MrJslbhtgF3yQ==",
|
"integrity": "sha512-LMAnruDqDmsjL6UoQD65aLotpmfO49Fsr3H0bMi7I+BH6jbv9OJiE61kN56daKsVtCQEt0SU1MrJslbhtgF3yQ==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"wheel-gestures": "^2.2.5"
|
"wheel-gestures": "^2.2.5"
|
||||||
},
|
},
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"embla-carousel": "^8.3.0",
|
||||||
"embla-carousel-react": "^8.3.0",
|
"embla-carousel-react": "^8.3.0",
|
||||||
"embla-carousel-wheel-gestures": "^8.0.1",
|
"embla-carousel-wheel-gestures": "^8.0.1",
|
||||||
"framer-motion": "^11.2.3",
|
"framer-motion": "^11.2.3",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// Import necessary modules
|
// Import necessary modules
|
||||||
import { io, Socket } from "socket.io-client";
|
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
|
import { io, Socket } from "socket.io-client";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ socketRef.on("connect", async () => {
|
|||||||
console.log("Connected to the server");
|
console.log("Connected to the server");
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
socketRef.emit("list", (response: CallbackResponse) => {
|
socketRef.emit("list", {}, (response: CallbackResponse) => {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
console.log("List of apps:", response.apps);
|
console.log("List of apps:", response.apps);
|
||||||
} else {
|
} else {
|
||||||
@ -29,7 +29,7 @@ socketRef.on("connect", async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socketRef.emit("deploy", (response: CallbackResponse) => {
|
socketRef.emit("deploy", {}, (response: CallbackResponse) => {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
console.log("It worked!");
|
console.log("It worked!");
|
||||||
} else {
|
} else {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user