Compare commits
23 Commits
feature/ai
...
fix/path-o
Author | SHA1 | Date | |
---|---|---|---|
2c058b259a | |||
5817b2ea48 | |||
6845e1fef9 | |||
f38919d6cf | |||
7aaa920815 | |||
48731848dd | |||
3fcfe5a3dc | |||
6e8eee246f | |||
982a6edc26 | |||
300de1f03a | |||
02deea9c93 | |||
653142dd1d | |||
ec24e64b17 | |||
b8398cc4c2 | |||
0e4649b2c9 | |||
d74205c909 | |||
fa998d9069 | |||
f5b04f9f49 | |||
169319de14 | |||
2dbdf51fd3 | |||
7b2ed21288 | |||
f4a84bd4b6 | |||
171a9ce3c6 |
@ -1,5 +1,4 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk";
|
||||
import { MessageParam } from "@anthropic-ai/sdk/src/resources/messages.js";
|
||||
|
||||
export interface Env {
|
||||
ANTHROPIC_API_KEY: string;
|
||||
@ -7,112 +6,69 @@ export interface Env {
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
// Handle CORS preflight requests
|
||||
if (request.method === "OPTIONS") {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (request.method !== "GET" && request.method !== "POST") {
|
||||
if (request.method !== "GET") {
|
||||
return new Response("Method Not Allowed", { status: 405 });
|
||||
}
|
||||
|
||||
let body;
|
||||
let isEditCodeWidget = false;
|
||||
if (request.method === "POST") {
|
||||
body = await request.json() as { messages: unknown; context: unknown; activeFileContent: string };
|
||||
} else {
|
||||
const url = new URL(request.url);
|
||||
const fileName = url.searchParams.get("fileName") || "";
|
||||
const code = url.searchParams.get("code") || "";
|
||||
const line = url.searchParams.get("line") || "";
|
||||
const instructions = url.searchParams.get("instructions") || "";
|
||||
const url = new URL(request.url);
|
||||
// const fileName = url.searchParams.get("fileName");
|
||||
// const line = url.searchParams.get("line");
|
||||
const instructions = url.searchParams.get("instructions");
|
||||
const code = url.searchParams.get("code");
|
||||
|
||||
body = {
|
||||
messages: [{ role: "human", content: instructions }],
|
||||
context: `File: ${fileName}\nLine: ${line}\nCode:\n${code}`,
|
||||
activeFileContent: code,
|
||||
};
|
||||
isEditCodeWidget = true;
|
||||
}
|
||||
const prompt = `
|
||||
Make the following changes to the code below:
|
||||
- ${instructions}
|
||||
|
||||
const messages = body.messages;
|
||||
const context = body.context;
|
||||
const activeFileContent = body.activeFileContent;
|
||||
Return the complete code chunk. Do not refer to other code files. Do not add code before or after the chunk. Start your reponse with \`\`\`, and end with \`\`\`. Do not include any other text.
|
||||
|
||||
if (!Array.isArray(messages) || messages.length === 0) {
|
||||
return new Response("Invalid or empty messages", { status: 400 });
|
||||
}
|
||||
|
||||
let systemMessage;
|
||||
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.
|
||||
|
||||
Context:
|
||||
${context}
|
||||
|
||||
Active File Content:
|
||||
${activeFileContent}
|
||||
|
||||
Instructions: ${messages[0].content}
|
||||
|
||||
Respond only with the modified code that can directly replace the existing code.`;
|
||||
} else {
|
||||
systemMessage = `You are an intelligent programming assistant. 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:
|
||||
|
||||
\`\`\`python
|
||||
print("Hello, World!")
|
||||
\`\`\`
|
||||
${code}
|
||||
\`\`\`
|
||||
`;
|
||||
console.log(prompt);
|
||||
|
||||
Provide a clear and concise explanation along with any code snippets. Keep your response brief and to the point.
|
||||
|
||||
${context ? `Context:\n${context}\n` : ''}
|
||||
${activeFileContent ? `Active File Content:\n${activeFileContent}\n` : ''}`;
|
||||
}
|
||||
|
||||
const anthropicMessages = messages.map(msg => ({
|
||||
role: msg.role === 'human' ? 'user' : 'assistant',
|
||||
content: msg.content
|
||||
})) as MessageParam[];
|
||||
|
||||
try {
|
||||
try {
|
||||
const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY });
|
||||
|
||||
const stream = await anthropic.messages.create({
|
||||
interface TextBlock {
|
||||
type: "text";
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface ToolUseBlock {
|
||||
type: "tool_use";
|
||||
tool_use: {
|
||||
// Add properties if needed
|
||||
};
|
||||
}
|
||||
|
||||
type ContentBlock = TextBlock | ToolUseBlock;
|
||||
|
||||
function getTextContent(content: ContentBlock[]): string {
|
||||
for (const block of content) {
|
||||
if (block.type === "text") {
|
||||
return block.text;
|
||||
}
|
||||
}
|
||||
return "No text content found";
|
||||
}
|
||||
|
||||
const response = await anthropic.messages.create({
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
max_tokens: 1024,
|
||||
system: systemMessage,
|
||||
messages: anthropicMessages,
|
||||
stream: true,
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
});
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const message = response.content as ContentBlock[];
|
||||
const textBlockContent = getTextContent(message);
|
||||
|
||||
const streamResponse = new ReadableStream({
|
||||
async start(controller) {
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.type === 'content_block_delta' && chunk.delta.type === 'text_delta') {
|
||||
const bytes = encoder.encode(chunk.delta.text);
|
||||
controller.enqueue(bytes);
|
||||
}
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
const pattern = /```[a-zA-Z]*\n([\s\S]*?)\n```/;
|
||||
const match = textBlockContent.match(pattern);
|
||||
|
||||
return new Response(streamResponse, {
|
||||
headers: {
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
},
|
||||
});
|
||||
const codeContent = match ? match[1] : "Error: Could not extract code.";
|
||||
|
||||
return new Response(JSON.stringify({ "response": codeContent }))
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
return new Response("Internal Server Error", { status: 500 });
|
||||
|
@ -6,15 +6,10 @@ import { createServer } from "http";
|
||||
import { Server } from "socket.io";
|
||||
import { DokkuClient } from "./DokkuClient";
|
||||
import { SecureGitClient, FileData } from "./SecureGitClient";
|
||||
import fs, { readFile } from "fs";
|
||||
import fs from "fs";
|
||||
|
||||
import { z } from "zod";
|
||||
import {
|
||||
TFile,
|
||||
TFileData,
|
||||
TFolder,
|
||||
User
|
||||
} from "./types";
|
||||
import { User } from "./types";
|
||||
import {
|
||||
createFile,
|
||||
deleteFile,
|
||||
@ -26,7 +21,7 @@ import {
|
||||
} from "./fileoperations";
|
||||
import { LockManager } from "./utils";
|
||||
|
||||
import { Sandbox, Filesystem, FilesystemEvent, EntryInfo, WatchHandle } from "e2b";
|
||||
import { Sandbox, Filesystem } from "e2b";
|
||||
|
||||
import { Terminal } from "./Terminal"
|
||||
|
||||
@ -39,21 +34,6 @@ import {
|
||||
saveFileRL,
|
||||
} from "./ratelimit";
|
||||
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('Uncaught Exception:', error);
|
||||
// Do not exit the process
|
||||
// You can add additional logging or recovery logic here
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||
// Do not exit the process
|
||||
// You can also handle the rejected promise here if needed
|
||||
});
|
||||
|
||||
// The amount of time in ms that a container will stay alive without a hearbeat.
|
||||
const CONTAINER_TIMEOUT = 60_000;
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app: Express = express();
|
||||
@ -75,14 +55,14 @@ const terminals: Record<string, Terminal> = {};
|
||||
|
||||
const dirName = "/home/user";
|
||||
|
||||
const moveFile = async (filesystem: Filesystem, filePath: string, newFilePath: string) => {
|
||||
try {
|
||||
const fileContents = await filesystem.read(filePath);
|
||||
await filesystem.write(newFilePath, fileContents);
|
||||
await filesystem.remove(filePath);
|
||||
} catch (e) {
|
||||
console.error(`Error moving file from ${filePath} to ${newFilePath}:`, e);
|
||||
}
|
||||
const moveFile = async (
|
||||
filesystem: Filesystem,
|
||||
filePath: string,
|
||||
newFilePath: string
|
||||
) => {
|
||||
const fileContents = await filesystem.read(filePath);
|
||||
await filesystem.write(newFilePath, fileContents);
|
||||
await filesystem.remove(filePath);
|
||||
};
|
||||
|
||||
io.use(async (socket, next) => {
|
||||
@ -177,13 +157,12 @@ io.on("connection", async (socket) => {
|
||||
}
|
||||
}
|
||||
|
||||
const createdContainer = await lockManager.acquireLock(data.sandboxId, async () => {
|
||||
await lockManager.acquireLock(data.sandboxId, async () => {
|
||||
try {
|
||||
// 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 });
|
||||
containers[data.sandboxId] = await Sandbox.create({ timeoutMs: 1200_000 });
|
||||
console.log("Created container ", data.sandboxId);
|
||||
return true;
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(`Error creating container ${data.sandboxId}:`, e);
|
||||
@ -191,188 +170,33 @@ io.on("connection", async (socket) => {
|
||||
}
|
||||
});
|
||||
|
||||
const sandboxFiles = await getSandboxFiles(data.sandboxId);
|
||||
const projectDirectory = path.posix.join(dirName, "projects", data.sandboxId);
|
||||
const containerFiles = containers[data.sandboxId].files;
|
||||
const fileWatchers: WatchHandle[] = [];
|
||||
|
||||
// Change the owner of the project directory to user
|
||||
const fixPermissions = async (projectDirectory: string) => {
|
||||
try {
|
||||
await containers[data.sandboxId].commands.run(
|
||||
`sudo chown -R user "${projectDirectory}"`
|
||||
);
|
||||
} catch (e: any) {
|
||||
console.log("Failed to fix permissions: " + e);
|
||||
}
|
||||
const fixPermissions = async () => {
|
||||
await containers[data.sandboxId].commands.run(
|
||||
`sudo chown -R user "${path.posix.join(dirName, "projects", data.sandboxId)}"`
|
||||
);
|
||||
};
|
||||
|
||||
// Check if the given path is a directory
|
||||
const isDirectory = async (projectDirectory: string): Promise<boolean> => {
|
||||
// Copy all files from the project to the container
|
||||
const sandboxFiles = await getSandboxFiles(data.sandboxId);
|
||||
const containerFiles = containers[data.sandboxId].files;
|
||||
const promises = sandboxFiles.fileData.map(async (file) => {
|
||||
try {
|
||||
const result = await containers[data.sandboxId].commands.run(
|
||||
`[ -d "${projectDirectory}" ] && echo "true" || echo "false"`
|
||||
);
|
||||
return result.stdout.trim() === "true";
|
||||
} catch (e: any) {
|
||||
console.log("Failed to check if directory: " + e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Only continue to container setup if a new container was created
|
||||
if (createdContainer) {
|
||||
|
||||
// Copy all files from the project to the container
|
||||
const promises = sandboxFiles.fileData.map(async (file) => {
|
||||
try {
|
||||
const filePath = path.posix.join(dirName, file.id);
|
||||
const parentDirectory = path.dirname(filePath);
|
||||
if (!containerFiles.exists(parentDirectory)) {
|
||||
await containerFiles.makeDir(parentDirectory);
|
||||
}
|
||||
await containerFiles.write(filePath, file.data);
|
||||
} catch (e: any) {
|
||||
console.log("Failed to create file: " + e);
|
||||
const filePath = path.posix.join(dirName, file.id);
|
||||
const parentDirectory = path.dirname(filePath);
|
||||
if (!containerFiles.exists(parentDirectory)) {
|
||||
await containerFiles.makeDir(parentDirectory);
|
||||
}
|
||||
});
|
||||
await Promise.all(promises);
|
||||
|
||||
// Make the logged in user the owner of all project files
|
||||
fixPermissions(projectDirectory);
|
||||
|
||||
}
|
||||
|
||||
// Start filesystem watcher for the project directory
|
||||
const watchDirectory = async (directory: string): Promise<WatchHandle | undefined> => {
|
||||
try {
|
||||
return await containerFiles.watch(directory, async (event: FilesystemEvent) => {
|
||||
try {
|
||||
|
||||
function removeDirName(path : string, dirName : string) {
|
||||
return path.startsWith(dirName) ? path.slice(dirName.length) : path;
|
||||
}
|
||||
|
||||
// This is the absolute file path in the container
|
||||
const containerFilePath = path.posix.join(directory, event.name);
|
||||
// This is the file path relative to the home directory
|
||||
const sandboxFilePath = removeDirName(containerFilePath, dirName + "/");
|
||||
// This is the directory being watched relative to the home directory
|
||||
const sandboxDirectory = removeDirName(directory, dirName + "/");
|
||||
|
||||
// Helper function to find a folder by id
|
||||
function findFolderById(files: (TFolder | TFile)[], folderId : string) {
|
||||
return files.find((file : TFolder | TFile) => file.type === "folder" && file.id === folderId);
|
||||
}
|
||||
|
||||
// A new file or directory was created.
|
||||
if (event.type === "create") {
|
||||
const folder = findFolderById(sandboxFiles.files, sandboxDirectory) as TFolder;
|
||||
const isDir = await isDirectory(containerFilePath);
|
||||
|
||||
const newItem = isDir
|
||||
? { id: sandboxFilePath, name: event.name, type: "folder", children: [] } as TFolder
|
||||
: { id: sandboxFilePath, name: event.name, type: "file" } as TFile;
|
||||
|
||||
if (folder) {
|
||||
// If the folder exists, add the new item (file/folder) as a child
|
||||
folder.children.push(newItem);
|
||||
} else {
|
||||
// If folder doesn't exist, add the new item to the root
|
||||
sandboxFiles.files.push(newItem);
|
||||
}
|
||||
|
||||
if (!isDir) {
|
||||
const fileData = await containers[data.sandboxId].files.read(containerFilePath);
|
||||
const fileContents = typeof fileData === "string" ? fileData : "";
|
||||
sandboxFiles.fileData.push({ id: sandboxFilePath, data: fileContents });
|
||||
}
|
||||
|
||||
console.log(`Create ${sandboxFilePath}`);
|
||||
}
|
||||
|
||||
// A file or directory was removed or renamed.
|
||||
else if (event.type === "remove" || event.type == "rename") {
|
||||
const folder = findFolderById(sandboxFiles.files, sandboxDirectory) as TFolder;
|
||||
const isDir = await isDirectory(containerFilePath);
|
||||
|
||||
const isFileMatch = (file: TFolder | TFile | TFileData) => file.id === sandboxFilePath || file.id.startsWith(containerFilePath + '/');
|
||||
|
||||
if (folder) {
|
||||
// Remove item from its parent folder
|
||||
folder.children = folder.children.filter((file: TFolder | TFile) => !isFileMatch(file));
|
||||
} else {
|
||||
// Remove from the root if it's not inside a folder
|
||||
sandboxFiles.files = sandboxFiles.files.filter((file: TFolder | TFile) => !isFileMatch(file));
|
||||
}
|
||||
|
||||
// Also remove any corresponding file data
|
||||
sandboxFiles.fileData = sandboxFiles.fileData.filter((file: TFileData) => !isFileMatch(file));
|
||||
|
||||
console.log(`Removed: ${sandboxFilePath}`);
|
||||
}
|
||||
|
||||
// The contents of a file were changed.
|
||||
else if (event.type === "write") {
|
||||
const folder = findFolderById(sandboxFiles.files, sandboxDirectory) as TFolder;
|
||||
const fileToWrite = sandboxFiles.fileData.find(file => file.id === sandboxFilePath);
|
||||
|
||||
if (fileToWrite) {
|
||||
fileToWrite.data = await containers[data.sandboxId].files.read(containerFilePath);
|
||||
console.log(`Write to ${sandboxFilePath}`);
|
||||
} else {
|
||||
// If the file is part of a folder structure, locate it and update its data
|
||||
const fileInFolder = folder?.children.find(file => file.id === sandboxFilePath);
|
||||
if (fileInFolder) {
|
||||
const fileData = await containers[data.sandboxId].files.read(containerFilePath);
|
||||
const fileContents = typeof fileData === "string" ? fileData : "";
|
||||
sandboxFiles.fileData.push({ id: sandboxFilePath, data: fileContents });
|
||||
console.log(`Write to ${sandboxFilePath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tell the client to reload the file list
|
||||
socket.emit("loaded", sandboxFiles.files);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error handling ${event.type} event for ${event.name}:`, error);
|
||||
}
|
||||
}, { "timeout": 0 } )
|
||||
} catch (error) {
|
||||
console.error(`Error watching filesystem:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
// Watch the project directory
|
||||
const handle = await watchDirectory(projectDirectory);
|
||||
// Keep track of watch handlers to close later
|
||||
if (handle) fileWatchers.push(handle);
|
||||
|
||||
// Watch all subdirectories of the project directory, but not deeper
|
||||
// This also means directories created after the container is created won't be watched
|
||||
const dirContent = await containerFiles.list(projectDirectory);
|
||||
await Promise.all(dirContent.map(async (item : EntryInfo) => {
|
||||
if (item.type === "dir") {
|
||||
console.log("Watching " + item.path);
|
||||
// Keep track of watch handlers to close later
|
||||
const handle = await watchDirectory(item.path);
|
||||
if (handle) fileWatchers.push(handle);
|
||||
}
|
||||
}))
|
||||
|
||||
socket.emit("loaded", sandboxFiles.files);
|
||||
|
||||
socket.on("heartbeat", async () => {
|
||||
try {
|
||||
// This keeps the container alive for another CONTAINER_TIMEOUT seconds.
|
||||
// The E2B docs are unclear, but the timeout is relative to the time of this method call.
|
||||
await containers[data.sandboxId].setTimeout(CONTAINER_TIMEOUT);
|
||||
await containerFiles.write(filePath, file.data);
|
||||
} catch (e: any) {
|
||||
console.error("Error setting timeout:", e);
|
||||
io.emit("error", `Error: set timeout. ${e.message ?? e}`);
|
||||
console.log("Failed to create file: " + e);
|
||||
}
|
||||
});
|
||||
await Promise.all(promises);
|
||||
|
||||
fixPermissions();
|
||||
|
||||
socket.emit("loaded", sandboxFiles.files);
|
||||
|
||||
socket.on("getFile", (fileId: string, callback) => {
|
||||
console.log(fileId);
|
||||
@ -425,7 +249,7 @@ io.on("connection", async (socket) => {
|
||||
path.posix.join(dirName, file.id),
|
||||
body
|
||||
);
|
||||
fixPermissions(projectDirectory);
|
||||
fixPermissions();
|
||||
} catch (e: any) {
|
||||
console.error("Error saving file:", e);
|
||||
io.emit("error", `Error: file saving. ${e.message ?? e}`);
|
||||
@ -447,7 +271,7 @@ io.on("connection", async (socket) => {
|
||||
path.posix.join(dirName, fileId),
|
||||
path.posix.join(dirName, newFileId)
|
||||
);
|
||||
fixPermissions(projectDirectory);
|
||||
fixPermissions();
|
||||
|
||||
file.id = newFileId;
|
||||
|
||||
@ -540,7 +364,7 @@ io.on("connection", async (socket) => {
|
||||
path.posix.join(dirName, id),
|
||||
""
|
||||
);
|
||||
fixPermissions(projectDirectory);
|
||||
fixPermissions();
|
||||
|
||||
sandboxFiles.files.push({
|
||||
id,
|
||||
@ -606,7 +430,7 @@ io.on("connection", async (socket) => {
|
||||
path.posix.join(dirName, fileId),
|
||||
path.posix.join(dirName, newFileId)
|
||||
);
|
||||
fixPermissions(projectDirectory);
|
||||
fixPermissions();
|
||||
await renameFile(fileId, newFileId, file.data);
|
||||
} catch (e: any) {
|
||||
console.error("Error renaming folder:", e);
|
||||
@ -675,8 +499,7 @@ io.on("connection", async (socket) => {
|
||||
|
||||
socket.on("createTerminal", async (id: string, callback) => {
|
||||
try {
|
||||
// Note: The number of terminals per window is limited on the frontend, but not backend
|
||||
if (terminals[id]) {
|
||||
if (terminals[id] || Object.keys(terminals).length >= 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -815,28 +638,12 @@ io.on("connection", async (socket) => {
|
||||
generateCodePromise,
|
||||
]);
|
||||
|
||||
if (!generateCodeResponse.ok) {
|
||||
throw new Error(`HTTP error! status: ${generateCodeResponse.status}`);
|
||||
}
|
||||
const json = await generateCodeResponse.json();
|
||||
|
||||
const reader = generateCodeResponse.body?.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let result = '';
|
||||
|
||||
if (reader) {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
result += decoder.decode(value, { stream: true });
|
||||
}
|
||||
}
|
||||
|
||||
// The result should now contain only the modified code
|
||||
callback({ response: result.trim(), success: true });
|
||||
callback({ response: json.response, success: true });
|
||||
} catch (e: any) {
|
||||
console.error("Error generating code:", e);
|
||||
io.emit("error", `Error: code generation. ${e.message ?? e}`);
|
||||
callback({ response: "Error generating code. Please try again.", success: false });
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -847,12 +654,26 @@ io.on("connection", async (socket) => {
|
||||
connections[data.sandboxId]--;
|
||||
}
|
||||
|
||||
// Stop watching file changes in the container
|
||||
Promise.all(fileWatchers.map(async (handle : WatchHandle) => {
|
||||
await handle.close();
|
||||
}));
|
||||
|
||||
if (data.isOwner && connections[data.sandboxId] <= 0) {
|
||||
await Promise.all(
|
||||
Object.entries(terminals).map(async ([key, terminal]) => {
|
||||
await terminal.close();
|
||||
delete terminals[key];
|
||||
})
|
||||
);
|
||||
|
||||
await lockManager.acquireLock(data.sandboxId, async () => {
|
||||
try {
|
||||
if (containers[data.sandboxId]) {
|
||||
await containers[data.sandboxId].kill();
|
||||
delete containers[data.sandboxId];
|
||||
console.log("Closed container", data.sandboxId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error closing container ", data.sandboxId, error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.broadcast.emit(
|
||||
"disableAccess",
|
||||
"The sandbox owner has disconnected."
|
||||
|
@ -36,7 +36,43 @@ import { createSandbox } from "@/lib/actions"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Loader2 } from "lucide-react"
|
||||
import { Button } from "../ui/button"
|
||||
import { projectTemplates } from "@/lib/data"
|
||||
|
||||
const data: {
|
||||
id: string
|
||||
name: string
|
||||
icon: string
|
||||
description: string
|
||||
disabled: boolean
|
||||
}[] = [
|
||||
{
|
||||
id: "reactjs",
|
||||
name: "React",
|
||||
icon: "/project-icons/react.svg",
|
||||
description: "A JavaScript library for building user interfaces",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
id: "vanillajs",
|
||||
name: "HTML/JS",
|
||||
icon: "/project-icons/more.svg",
|
||||
description: "More coming soon, feel free to contribute on GitHub",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
id: "nextjs",
|
||||
name: "NextJS",
|
||||
icon: "/project-icons/node.svg",
|
||||
description: "A JavaScript runtime built on the V8 JavaScript engine",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
id: "streamlit",
|
||||
name: "Streamlit",
|
||||
icon: "/project-icons/python.svg",
|
||||
description: "A JavaScript runtime built on the V8 JavaScript engine",
|
||||
disabled: false,
|
||||
}
|
||||
]
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z
|
||||
@ -88,12 +124,12 @@ export default function NewProjectModal({
|
||||
if (!loading) setOpen(open)
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-h-[95vh] overflow-y-auto">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create A Sandbox</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid grid-cols-2 w-full gap-2 mt-2">
|
||||
{projectTemplates.map((item) => (
|
||||
{data.map((item) => (
|
||||
<button
|
||||
disabled={item.disabled || loading}
|
||||
key={item.id}
|
||||
|
@ -8,7 +8,6 @@ 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({
|
||||
children,
|
||||
@ -44,9 +43,7 @@ export default function ProjectCard({
|
||||
setDate(`${Math.floor(diffInMinutes / 1440)}d ago`)
|
||||
}
|
||||
}, [sandbox])
|
||||
const projectIcon =
|
||||
projectTemplates.find((p) => p.id === sandbox.type)?.icon ??
|
||||
"/project-icons/node.svg"
|
||||
|
||||
return (
|
||||
<Card
|
||||
tabIndex={0}
|
||||
@ -68,7 +65,16 @@ export default function ProjectCard({
|
||||
</AnimatePresence>
|
||||
|
||||
<div className="space-x-2 flex items-center justify-start w-full z-10">
|
||||
<Image alt="" src={projectIcon} width={20} height={20} />
|
||||
<Image
|
||||
alt=""
|
||||
src={
|
||||
sandbox.type === "react"
|
||||
? "/project-icons/react.svg"
|
||||
: "/project-icons/node.svg"
|
||||
}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
<div className="font-medium static whitespace-nowrap w-full text-ellipsis overflow-hidden">
|
||||
{sandbox.name}
|
||||
</div>
|
||||
|
@ -1,36 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Button } from '../../ui/button';
|
||||
import { Send, StopCircle } from 'lucide-react';
|
||||
|
||||
interface ChatInputProps {
|
||||
input: string;
|
||||
setInput: (input: string) => void;
|
||||
isGenerating: boolean;
|
||||
handleSend: () => void;
|
||||
handleStopGeneration: () => void;
|
||||
}
|
||||
|
||||
export default function ChatInput({ input, setInput, isGenerating, handleSend, handleStopGeneration }: ChatInputProps) {
|
||||
return (
|
||||
<div className="flex space-x-2 min-w-0">
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && !isGenerating && handleSend()}
|
||||
className="flex-grow p-2 border rounded-lg min-w-0 bg-input"
|
||||
placeholder="Type your message..."
|
||||
disabled={isGenerating}
|
||||
/>
|
||||
{isGenerating ? (
|
||||
<Button onClick={handleStopGeneration} variant="destructive" size="icon" className="h-10 w-10">
|
||||
<StopCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={handleSend} disabled={isGenerating} size="icon" className="h-10 w-10">
|
||||
<Send className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,201 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '../../ui/button';
|
||||
import { ChevronUp, ChevronDown, Copy, Check, CornerUpLeft } from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { copyToClipboard, stringifyContent } from './lib/chatUtils';
|
||||
|
||||
interface MessageProps {
|
||||
message: {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
context?: string;
|
||||
};
|
||||
setContext: (context: string | null) => void;
|
||||
setIsContextExpanded: (isExpanded: boolean) => void;
|
||||
}
|
||||
|
||||
export default function ChatMessage({ message, setContext, setIsContextExpanded }: MessageProps) {
|
||||
const [expandedMessageIndex, setExpandedMessageIndex] = useState<number | null>(null);
|
||||
const [copiedText, setCopiedText] = useState<string | null>(null);
|
||||
|
||||
const renderCopyButton = (text: any) => (
|
||||
<Button
|
||||
onClick={() => copyToClipboard(stringifyContent(text), setCopiedText)}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="p-1 h-6"
|
||||
>
|
||||
{copiedText === stringifyContent(text) ? (
|
||||
<Check className="w-4 h-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
|
||||
const askAboutCode = (code: any) => {
|
||||
const contextString = stringifyContent(code);
|
||||
setContext(`Regarding this code:\n${contextString}`);
|
||||
setIsContextExpanded(false);
|
||||
};
|
||||
|
||||
const renderMarkdownElement = (props: any) => {
|
||||
const { node, children } = props;
|
||||
const content = stringifyContent(children);
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
<div className="absolute top-0 right-0 flex opacity-0 group-hover:opacity-30 transition-opacity">
|
||||
{renderCopyButton(content)}
|
||||
<Button
|
||||
onClick={() => askAboutCode(content)}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="p-1 h-6"
|
||||
>
|
||||
<CornerUpLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{React.createElement(node.tagName, {
|
||||
...props,
|
||||
className: `${props.className || ''} hover:bg-transparent rounded p-1 transition-colors`
|
||||
}, children)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-left relative">
|
||||
<div className={`relative p-2 rounded-lg ${
|
||||
message.role === 'user'
|
||||
? 'bg-[#262626] text-white'
|
||||
: 'bg-transparent text-white'
|
||||
} max-w-full`}>
|
||||
{message.role === 'user' && (
|
||||
<div className="absolute top-0 right-0 flex opacity-0 group-hover:opacity-30 transition-opacity">
|
||||
{renderCopyButton(message.content)}
|
||||
<Button
|
||||
onClick={() => askAboutCode(message.content)}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="p-1 h-6"
|
||||
>
|
||||
<CornerUpLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{message.context && (
|
||||
<div className="mb-2 bg-input rounded-lg">
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() => setExpandedMessageIndex(expandedMessageIndex === 0 ? null : 0)}
|
||||
>
|
||||
<span className="text-sm text-gray-300">
|
||||
Context
|
||||
</span>
|
||||
{expandedMessageIndex === 0 ? (
|
||||
<ChevronUp size={16} />
|
||||
) : (
|
||||
<ChevronDown size={16} />
|
||||
)}
|
||||
</div>
|
||||
{expandedMessageIndex === 0 && (
|
||||
<div className="relative">
|
||||
<div className="absolute top-0 right-0 flex p-1">
|
||||
{renderCopyButton(message.context.replace(/^Regarding this code:\n/, ''))}
|
||||
</div>
|
||||
{(() => {
|
||||
const code = message.context.replace(/^Regarding this code:\n/, '');
|
||||
const match = /language-(\w+)/.exec(code);
|
||||
const language = match ? match[1] : 'typescript';
|
||||
return (
|
||||
<div className="pt-6">
|
||||
<textarea
|
||||
value={code}
|
||||
onChange={(e) => {
|
||||
const updatedContext = `Regarding this code:\n${e.target.value}`;
|
||||
setContext(updatedContext);
|
||||
}}
|
||||
className="w-full p-2 bg-[#1e1e1e] text-white font-mono text-sm rounded"
|
||||
rows={code.split('\n').length}
|
||||
style={{
|
||||
resize: 'vertical',
|
||||
minHeight: '100px',
|
||||
maxHeight: '400px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{message.role === 'assistant' ? (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
code({node, className, children, ...props}) {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
return match ? (
|
||||
<div className="relative border border-input rounded-md my-4">
|
||||
<div className="absolute top-0 left-0 px-2 py-1 text-xs font-semibold text-gray-200 bg-#1e1e1e rounded-tl">
|
||||
{match[1]}
|
||||
</div>
|
||||
<div className="absolute top-0 right-0 flex">
|
||||
{renderCopyButton(children)}
|
||||
<Button
|
||||
onClick={() => askAboutCode(children)}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="p-1 h-6"
|
||||
>
|
||||
<CornerUpLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="pt-6">
|
||||
<SyntaxHighlighter
|
||||
style={vscDarkPlus as any}
|
||||
language={match[1]}
|
||||
PreTag="div"
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: '0.5rem',
|
||||
fontSize: '0.875rem',
|
||||
}}
|
||||
>
|
||||
{stringifyContent(children)}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
p: renderMarkdownElement,
|
||||
h1: renderMarkdownElement,
|
||||
h2: renderMarkdownElement,
|
||||
h3: renderMarkdownElement,
|
||||
h4: renderMarkdownElement,
|
||||
h5: renderMarkdownElement,
|
||||
h6: renderMarkdownElement,
|
||||
ul: (props) => <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}
|
||||
</ReactMarkdown>
|
||||
) : (
|
||||
<div className="whitespace-pre-wrap group">
|
||||
{message.content}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
import React from 'react';
|
||||
import { ChevronUp, ChevronDown, X } from 'lucide-react';
|
||||
|
||||
interface ContextDisplayProps {
|
||||
context: string | null;
|
||||
isContextExpanded: boolean;
|
||||
setIsContextExpanded: (isExpanded: boolean) => void;
|
||||
setContext: (context: string | null) => void;
|
||||
}
|
||||
|
||||
export default function ContextDisplay({ context, isContextExpanded, setIsContextExpanded, setContext }: ContextDisplayProps) {
|
||||
if (!context) return null;
|
||||
|
||||
return (
|
||||
<div className="mb-2 bg-input p-2 rounded-lg">
|
||||
<div className="flex justify-between items-center">
|
||||
<div
|
||||
className="flex-grow cursor-pointer"
|
||||
onClick={() => setIsContextExpanded(!isContextExpanded)}
|
||||
>
|
||||
<span className="text-sm text-gray-300">
|
||||
Context
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
{isContextExpanded ? (
|
||||
<ChevronUp size={16} className="cursor-pointer" onClick={() => setIsContextExpanded(false)} />
|
||||
) : (
|
||||
<ChevronDown size={16} className="cursor-pointer" onClick={() => setIsContextExpanded(true)} />
|
||||
)}
|
||||
<X
|
||||
size={16}
|
||||
className="ml-2 cursor-pointer text-gray-400 hover:text-gray-200"
|
||||
onClick={() => setContext(null)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isContextExpanded && (
|
||||
<textarea
|
||||
value={context.replace(/^Regarding this code:\n/, '')}
|
||||
onChange={(e) => setContext(`Regarding this code:\n${e.target.value}`)}
|
||||
className="w-full mt-2 p-2 bg-#1e1e1e text-white rounded"
|
||||
rows={5}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import LoadingDots from '../../ui/LoadingDots';
|
||||
import ChatMessage from './ChatMessage';
|
||||
import ChatInput from './ChatInput';
|
||||
import ContextDisplay from './ContextDisplay';
|
||||
import { handleSend, handleStopGeneration } from './lib/chatUtils';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
interface Message {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
context?: string;
|
||||
}
|
||||
|
||||
export default function AIChat({ activeFileContent, activeFileName, onClose }: { activeFileContent: string, activeFileName: string, onClose: () => void }) {
|
||||
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(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (chatContainerRef.current) {
|
||||
setTimeout(() => {
|
||||
chatContainerRef.current?.scrollTo({
|
||||
top: chatContainerRef.current.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen w-full">
|
||||
<div className="flex justify-between items-center p-2 border-b">
|
||||
<span className="text-muted-foreground/50 font-medium">CHAT</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">
|
||||
{messages.map((message, messageIndex) => (
|
||||
<ChatMessage
|
||||
key={messageIndex}
|
||||
message={message}
|
||||
setContext={setContext}
|
||||
setIsContextExpanded={setIsContextExpanded}
|
||||
/>
|
||||
))}
|
||||
{isLoading && <LoadingDots />}
|
||||
</div>
|
||||
<div className="p-4 border-t mb-14">
|
||||
<ContextDisplay
|
||||
context={context}
|
||||
isContextExpanded={isContextExpanded}
|
||||
setIsContextExpanded={setIsContextExpanded}
|
||||
setContext={setContext}
|
||||
/>
|
||||
<ChatInput
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
isGenerating={isGenerating}
|
||||
handleSend={() => handleSend(input, context, messages, setMessages, setInput, setIsContextExpanded, setIsGenerating, setIsLoading, abortControllerRef, activeFileContent)}
|
||||
handleStopGeneration={() => handleStopGeneration(abortControllerRef)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,162 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export const stringifyContent = (content: any, seen = new WeakSet()): string => {
|
||||
if (typeof content === 'string') {
|
||||
return content;
|
||||
}
|
||||
if (content === null) {
|
||||
return 'null';
|
||||
}
|
||||
if (content === undefined) {
|
||||
return 'undefined';
|
||||
}
|
||||
if (typeof content === 'number' || typeof content === 'boolean') {
|
||||
return content.toString();
|
||||
}
|
||||
if (typeof content === 'function') {
|
||||
return content.toString();
|
||||
}
|
||||
if (typeof content === 'symbol') {
|
||||
return content.toString();
|
||||
}
|
||||
if (typeof content === 'bigint') {
|
||||
return content.toString() + 'n';
|
||||
}
|
||||
if (React.isValidElement(content)) {
|
||||
return React.Children.toArray((content as React.ReactElement).props.children)
|
||||
.map(child => stringifyContent(child, seen))
|
||||
.join('');
|
||||
}
|
||||
if (Array.isArray(content)) {
|
||||
return '[' + content.map(item => stringifyContent(item, seen)).join(', ') + ']';
|
||||
}
|
||||
if (typeof content === 'object') {
|
||||
if (seen.has(content)) {
|
||||
return '[Circular]';
|
||||
}
|
||||
seen.add(content);
|
||||
try {
|
||||
const pairs = Object.entries(content).map(
|
||||
([key, value]) => `${key}: ${stringifyContent(value, seen)}`
|
||||
);
|
||||
return '{' + pairs.join(', ') + '}';
|
||||
} catch (error) {
|
||||
return Object.prototype.toString.call(content);
|
||||
}
|
||||
}
|
||||
return String(content);
|
||||
};
|
||||
|
||||
export const copyToClipboard = (text: string, setCopiedText: (text: string | null) => void) => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
setCopiedText(text);
|
||||
setTimeout(() => setCopiedText(null), 2000);
|
||||
});
|
||||
};
|
||||
|
||||
export const handleSend = async (
|
||||
input: string,
|
||||
context: string | null,
|
||||
messages: any[],
|
||||
setMessages: React.Dispatch<React.SetStateAction<any[]>>,
|
||||
setInput: React.Dispatch<React.SetStateAction<string>>,
|
||||
setIsContextExpanded: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
setIsGenerating: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
abortControllerRef: React.MutableRefObject<AbortController | null>,
|
||||
activeFileContent: string
|
||||
) => {
|
||||
if (input.trim() === '' && !context) return;
|
||||
|
||||
const newMessage = {
|
||||
role: 'user' as const,
|
||||
content: input,
|
||||
context: context || undefined
|
||||
};
|
||||
const updatedMessages = [...messages, newMessage];
|
||||
setMessages(updatedMessages);
|
||||
setInput('');
|
||||
setIsContextExpanded(false);
|
||||
setIsGenerating(true);
|
||||
setIsLoading(true);
|
||||
|
||||
abortControllerRef.current = new AbortController();
|
||||
|
||||
try {
|
||||
const anthropicMessages = updatedMessages.map(msg => ({
|
||||
role: msg.role === 'user' ? 'human' : 'assistant',
|
||||
content: msg.content
|
||||
}));
|
||||
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_AI_WORKER_URL}/api`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: anthropicMessages,
|
||||
context: context || undefined,
|
||||
activeFileContent: activeFileContent,
|
||||
}),
|
||||
signal: abortControllerRef.current.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to get AI response');
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
const assistantMessage = { role: 'assistant' as const, content: '' };
|
||||
setMessages([...updatedMessages, assistantMessage]);
|
||||
setIsLoading(false);
|
||||
|
||||
let buffer = '';
|
||||
const updateInterval = 100;
|
||||
let lastUpdateTime = Date.now();
|
||||
|
||||
if (reader) {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - lastUpdateTime > updateInterval) {
|
||||
setMessages(prev => {
|
||||
const updatedMessages = [...prev];
|
||||
const lastMessage = updatedMessages[updatedMessages.length - 1];
|
||||
lastMessage.content = buffer;
|
||||
return updatedMessages;
|
||||
});
|
||||
lastUpdateTime = currentTime;
|
||||
}
|
||||
}
|
||||
|
||||
setMessages(prev => {
|
||||
const updatedMessages = [...prev];
|
||||
const lastMessage = updatedMessages[updatedMessages.length - 1];
|
||||
lastMessage.content = buffer;
|
||||
return updatedMessages;
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.name === 'AbortError') {
|
||||
console.log('Generation aborted');
|
||||
} else {
|
||||
console.error('Error fetching AI response:', error);
|
||||
const errorMessage = { role: 'assistant' as const, content: 'Sorry, I encountered an error. Please try again.' };
|
||||
setMessages(prev => [...prev, errorMessage]);
|
||||
}
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
setIsLoading(false);
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
export const handleStopGeneration = (abortControllerRef: React.MutableRefObject<AbortController | null>) => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
};
|
@ -18,7 +18,7 @@ import {
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "@/components/ui/resizable"
|
||||
import { FileJson, Loader2, Sparkles, TerminalSquare, ArrowDownToLine, ArrowRightToLine } from "lucide-react"
|
||||
import { FileJson, Loader2, Sparkles, TerminalSquare } from "lucide-react"
|
||||
import Tab from "../ui/tab"
|
||||
import Sidebar from "./sidebar"
|
||||
import GenerateInput from "./generate"
|
||||
@ -35,9 +35,6 @@ import { PreviewProvider, usePreview } from "@/context/PreviewContext"
|
||||
import { useSocket } from "@/context/SocketContext"
|
||||
import { Button } from "../ui/button"
|
||||
import React from "react"
|
||||
import { parseTSConfigToMonacoOptions } from "@/lib/tsconfig"
|
||||
import { deepMerge } from "@/lib/utils"
|
||||
import AIChat from "./AIChat"
|
||||
|
||||
export default function CodeEditor({
|
||||
userData,
|
||||
@ -60,13 +57,6 @@ export default function CodeEditor({
|
||||
}
|
||||
}, [socket, userData.id, sandboxData.id, setUserAndSandboxId])
|
||||
|
||||
// This heartbeat is critical to preventing the E2B sandbox from timing out
|
||||
useEffect(() => {
|
||||
// 10000 ms = 10 seconds
|
||||
const interval = setInterval(() => socket?.emit("heartbeat"), 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, [socket]);
|
||||
|
||||
//Preview Button state
|
||||
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
|
||||
const [disableAccess, setDisableAccess] = useState({
|
||||
@ -74,13 +64,6 @@ export default function CodeEditor({
|
||||
message: "",
|
||||
})
|
||||
|
||||
// Layout state
|
||||
const [isHorizontalLayout, setIsHorizontalLayout] = useState(false);
|
||||
const [previousLayout, setPreviousLayout] = useState(false);
|
||||
|
||||
// AI Chat state
|
||||
const [isAIChatOpen, setIsAIChatOpen] = useState(false);
|
||||
|
||||
// File state
|
||||
const [files, setFiles] = useState<(TFolder | TFile)[]>([])
|
||||
const [tabs, setTabs] = useState<TTab[]>([])
|
||||
@ -153,7 +136,7 @@ export default function CodeEditor({
|
||||
const generateRef = useRef<HTMLDivElement>(null)
|
||||
const suggestionRef = useRef<HTMLDivElement>(null)
|
||||
const generateWidgetRef = useRef<HTMLDivElement>(null)
|
||||
const { previewPanelRef } = usePreview();
|
||||
const previewPanelRef = useRef<ImperativePanelHandle>(null)
|
||||
const editorPanelRef = useRef<ImperativePanelHandle>(null)
|
||||
const previewWindowRef = useRef<{ refreshIframe: () => void }>(null)
|
||||
|
||||
@ -173,78 +156,9 @@ export default function CodeEditor({
|
||||
}
|
||||
|
||||
// Post-mount editor keybindings and actions
|
||||
const handleEditorMount: OnMount = async (editor, monaco) => {
|
||||
const handleEditorMount: OnMount = (editor, monaco) => {
|
||||
setEditorRef(editor)
|
||||
monacoRef.current = monaco
|
||||
/**
|
||||
* Sync all the models to the worker eagerly.
|
||||
* This enables intelliSense for all files without needing an `addExtraLib` call.
|
||||
*/
|
||||
monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true)
|
||||
monaco.languages.typescript.javascriptDefaults.setEagerModelSync(true)
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions(
|
||||
defaultCompilerOptions
|
||||
)
|
||||
monaco.languages.typescript.javascriptDefaults.setCompilerOptions(
|
||||
defaultCompilerOptions
|
||||
)
|
||||
const fetchFileContent = (fileId: string): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
socket?.emit("getFile", fileId, (content: string) => {
|
||||
resolve(content)
|
||||
})
|
||||
})
|
||||
}
|
||||
const loadTSConfig = async (files: (TFolder | TFile)[]) => {
|
||||
const tsconfigFiles = files.filter((file) =>
|
||||
file.name.endsWith("tsconfig.json")
|
||||
)
|
||||
let mergedConfig: any = { compilerOptions: {} }
|
||||
|
||||
for (const file of tsconfigFiles) {
|
||||
const containerId = file.id.split("/").slice(0, 2).join("/")
|
||||
const content = await fetchFileContent(file.id)
|
||||
|
||||
try {
|
||||
let tsConfig = JSON.parse(content)
|
||||
|
||||
// Handle references
|
||||
if (tsConfig.references) {
|
||||
for (const ref of tsConfig.references) {
|
||||
const path = ref.path.replace("./", "")
|
||||
const fileId = `${containerId}/${path}`
|
||||
const refContent = await fetchFileContent(fileId)
|
||||
const referenceTsConfig = JSON.parse(refContent)
|
||||
|
||||
// Merge configurations
|
||||
mergedConfig = deepMerge(mergedConfig, referenceTsConfig)
|
||||
}
|
||||
}
|
||||
|
||||
// Merge current file's config
|
||||
mergedConfig = deepMerge(mergedConfig, tsConfig)
|
||||
} catch (error) {
|
||||
console.error("Error parsing TSConfig:", error)
|
||||
}
|
||||
}
|
||||
// Apply merged compiler options
|
||||
if (mergedConfig.compilerOptions) {
|
||||
const updatedOptions = parseTSConfigToMonacoOptions({
|
||||
...defaultCompilerOptions,
|
||||
...mergedConfig.compilerOptions,
|
||||
})
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions(
|
||||
updatedOptions
|
||||
)
|
||||
monaco.languages.typescript.javascriptDefaults.setCompilerOptions(
|
||||
updatedOptions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Call the function with your file structure
|
||||
await loadTSConfig(files)
|
||||
|
||||
editor.onDidChangeCursorPosition((e) => {
|
||||
setIsSelected(false)
|
||||
@ -424,7 +338,7 @@ export default function CodeEditor({
|
||||
})
|
||||
}
|
||||
}, [generate.show])
|
||||
|
||||
|
||||
// Suggestion widget effect
|
||||
useEffect(() => {
|
||||
if (!suggestionRef.current || !editorRef) return
|
||||
@ -521,29 +435,20 @@ export default function CodeEditor({
|
||||
[socket, fileContents]
|
||||
);
|
||||
|
||||
// Keydown event listener to trigger file save on Ctrl+S or Cmd+S, and toggle AI chat on Ctrl+L or Cmd+L
|
||||
// Keydown event listener to trigger file save on Ctrl+S or Cmd+S
|
||||
useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault()
|
||||
debouncedSaveData(activeFileId);
|
||||
} else if (e.key === "l" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault()
|
||||
setIsAIChatOpen(prev => !prev);
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
document.addEventListener("keydown", down)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", down)
|
||||
}
|
||||
}, [activeFileId, tabs, debouncedSaveData, setIsAIChatOpen, editorRef])
|
||||
}, [activeFileId, tabs, debouncedSaveData])
|
||||
|
||||
// Liveblocks live collaboration setup effect
|
||||
useEffect(() => {
|
||||
@ -703,9 +608,9 @@ export default function CodeEditor({
|
||||
|
||||
const selectFile = (tab: TTab) => {
|
||||
if (tab.id === activeFileId) return;
|
||||
|
||||
|
||||
setGenerate((prev) => ({ ...prev, show: false }));
|
||||
|
||||
|
||||
// Check if the tab already exists in the list of open tabs
|
||||
const exists = tabs.find((t) => t.id === tab.id);
|
||||
setTabs((prev) => {
|
||||
@ -717,7 +622,7 @@ export default function CodeEditor({
|
||||
// If the tab doesn't exist, add it to the list of tabs and make it active
|
||||
return [...prev, tab];
|
||||
});
|
||||
|
||||
|
||||
// If the file's content is already cached, set it as the active content
|
||||
if (fileContents[tab.id]) {
|
||||
setActiveFileContent(fileContents[tab.id]);
|
||||
@ -728,7 +633,7 @@ export default function CodeEditor({
|
||||
setActiveFileContent(response);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Set the editor language based on the file type
|
||||
setEditorLanguage(processFileType(tab.name));
|
||||
// Set the active file ID to the new tab
|
||||
@ -844,37 +749,6 @@ export default function CodeEditor({
|
||||
})
|
||||
}
|
||||
|
||||
const togglePreviewPanel = () => {
|
||||
if (isPreviewCollapsed) {
|
||||
previewPanelRef.current?.expand();
|
||||
setIsPreviewCollapsed(false);
|
||||
} else {
|
||||
previewPanelRef.current?.collapse();
|
||||
setIsPreviewCollapsed(true);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleLayout = () => {
|
||||
if (!isAIChatOpen) {
|
||||
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
|
||||
if (disableAccess.isDisabled)
|
||||
return (
|
||||
@ -994,6 +868,7 @@ export default function CodeEditor({
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Main editor components */}
|
||||
<Sidebar
|
||||
sandboxData={sandboxData}
|
||||
@ -1007,199 +882,145 @@ export default function CodeEditor({
|
||||
addNew={(name, type) => addNew(name, type, setFiles, sandboxData)}
|
||||
deletingFolderId={deletingFolderId}
|
||||
/>
|
||||
{/* Outer ResizablePanelGroup for main layout */}
|
||||
<ResizablePanelGroup direction={isHorizontalLayout ? "horizontal" : "vertical"}>
|
||||
{/* Left side: Editor and Preview/Terminal */}
|
||||
<ResizablePanel defaultSize={isAIChatOpen ? 80 : 100} minSize={50}>
|
||||
<ResizablePanelGroup direction={isHorizontalLayout ? "vertical" : "horizontal"}>
|
||||
<ResizablePanel
|
||||
className="p-2 flex flex-col"
|
||||
maxSize={80}
|
||||
minSize={30}
|
||||
defaultSize={70}
|
||||
ref={editorPanelRef}
|
||||
>
|
||||
<div className="h-10 w-full flex gap-2 overflow-auto tab-scroll">
|
||||
{/* File tabs */}
|
||||
{tabs.map((tab) => (
|
||||
<Tab
|
||||
key={tab.id}
|
||||
saved={tab.saved}
|
||||
selected={activeFileId === tab.id}
|
||||
onClick={(e) => {
|
||||
selectFile(tab)
|
||||
}}
|
||||
onClose={() => closeTab(tab.id)}
|
||||
>
|
||||
{tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
</div>
|
||||
{/* Monaco editor */}
|
||||
<div
|
||||
ref={editorContainerRef}
|
||||
className="grow w-full overflow-hidden rounded-md relative"
|
||||
|
||||
{/* Shadcn resizeable panels: https://ui.shadcn.com/docs/components/resizable */}
|
||||
<ResizablePanelGroup direction="horizontal">
|
||||
<ResizablePanel
|
||||
className="p-2 flex flex-col"
|
||||
maxSize={80}
|
||||
minSize={30}
|
||||
defaultSize={60}
|
||||
ref={editorPanelRef}
|
||||
>
|
||||
<div className="h-10 w-full flex gap-2 overflow-auto tab-scroll">
|
||||
{/* File tabs */}
|
||||
{tabs.map((tab) => (
|
||||
<Tab
|
||||
key={tab.id}
|
||||
saved={tab.saved}
|
||||
selected={activeFileId === tab.id}
|
||||
onClick={(e) => {
|
||||
selectFile(tab)
|
||||
}}
|
||||
onClose={() => closeTab(tab.id)}
|
||||
>
|
||||
{!activeFileId ? (
|
||||
<>
|
||||
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
|
||||
<FileJson className="w-6 h-6 mr-3" />
|
||||
No file selected.
|
||||
</div>
|
||||
</>
|
||||
) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643
|
||||
clerk.loaded ? (
|
||||
<>
|
||||
{provider && userInfo ? (
|
||||
<Cursors yProvider={provider} userInfo={userInfo} />
|
||||
) : null}
|
||||
<Editor
|
||||
height="100%"
|
||||
language={editorLanguage}
|
||||
beforeMount={handleEditorWillMount}
|
||||
onMount={handleEditorMount}
|
||||
onChange={(value) => {
|
||||
// If the new content is different from the cached content, update it
|
||||
if (value !== fileContents[activeFileId]) {
|
||||
setActiveFileContent(value ?? ""); // Update the active file content
|
||||
// Mark the file as unsaved by setting 'saved' to false
|
||||
setTabs((prev) =>
|
||||
prev.map((tab) =>
|
||||
tab.id === activeFileId
|
||||
? { ...tab, saved: false }
|
||||
: tab
|
||||
)
|
||||
)
|
||||
} else {
|
||||
// If the content matches the cached content, mark the file as saved
|
||||
setTabs((prev) =>
|
||||
prev.map((tab) =>
|
||||
tab.id === activeFileId
|
||||
? { ...tab, saved: true }
|
||||
: tab
|
||||
)
|
||||
)
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
tabSize: 2,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
padding: {
|
||||
bottom: 4,
|
||||
top: 4,
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
fixedOverflowWidgets: true,
|
||||
fontFamily: "var(--font-geist-mono)",
|
||||
}}
|
||||
theme="vs-dark"
|
||||
value={activeFileContent}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
|
||||
<Loader2 className="animate-spin w-6 h-6 mr-3" />
|
||||
Waiting for Clerk to load...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
</div>
|
||||
{/* Monaco editor */}
|
||||
<div
|
||||
ref={editorContainerRef}
|
||||
className="grow w-full overflow-hidden rounded-md relative"
|
||||
>
|
||||
{!activeFileId ? (
|
||||
<>
|
||||
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
|
||||
<FileJson className="w-6 h-6 mr-3" />
|
||||
No file selected.
|
||||
</div>
|
||||
</>
|
||||
) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643
|
||||
clerk.loaded ? (
|
||||
<>
|
||||
{provider && userInfo ? (
|
||||
<Cursors yProvider={provider} userInfo={userInfo} />
|
||||
) : null}
|
||||
<Editor
|
||||
height="100%"
|
||||
language={editorLanguage}
|
||||
beforeMount={handleEditorWillMount}
|
||||
onMount={handleEditorMount}
|
||||
onChange={(value) => {
|
||||
// If the new content is different from the cached content, update it
|
||||
if (value !== fileContents[activeFileId]) {
|
||||
setActiveFileContent(value ?? ""); // Update the active file content
|
||||
// Mark the file as unsaved by setting 'saved' to false
|
||||
setTabs((prev) =>
|
||||
prev.map((tab) =>
|
||||
tab.id === activeFileId
|
||||
? { ...tab, saved: false }
|
||||
: tab
|
||||
)
|
||||
)
|
||||
} else {
|
||||
// If the content matches the cached content, mark the file as saved
|
||||
setTabs((prev) =>
|
||||
prev.map((tab) =>
|
||||
tab.id === activeFileId
|
||||
? { ...tab, saved: true }
|
||||
: tab
|
||||
)
|
||||
)
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
tabSize: 2,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
padding: {
|
||||
bottom: 4,
|
||||
top: 4,
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
fixedOverflowWidgets: true,
|
||||
fontFamily: "var(--font-geist-mono)",
|
||||
}}
|
||||
theme="vs-dark"
|
||||
value={activeFileContent}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
|
||||
<Loader2 className="animate-spin w-6 h-6 mr-3" />
|
||||
Waiting for Clerk to load...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle />
|
||||
<ResizablePanel defaultSize={40}>
|
||||
<ResizablePanelGroup direction="vertical">
|
||||
<ResizablePanel
|
||||
ref={usePreview().previewPanelRef}
|
||||
defaultSize={4}
|
||||
collapsedSize={4}
|
||||
minSize={25}
|
||||
collapsible
|
||||
className="p-2 flex flex-col"
|
||||
onCollapse={() => setIsPreviewCollapsed(true)}
|
||||
onExpand={() => setIsPreviewCollapsed(false)}
|
||||
>
|
||||
<PreviewWindow
|
||||
open={() => {
|
||||
usePreview().previewPanelRef.current?.expand()
|
||||
setIsPreviewCollapsed(false)
|
||||
}}
|
||||
collapsed={isPreviewCollapsed}
|
||||
src={previewURL}
|
||||
ref={previewWindowRef}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle />
|
||||
<ResizablePanel defaultSize={30}>
|
||||
<ResizablePanelGroup direction={
|
||||
isAIChatOpen && isHorizontalLayout ? "horizontal" :
|
||||
isAIChatOpen ? "vertical" :
|
||||
isHorizontalLayout ? "horizontal" :
|
||||
"vertical"
|
||||
}>
|
||||
<ResizablePanel
|
||||
ref={previewPanelRef}
|
||||
defaultSize={isPreviewCollapsed ? 4 : 20}
|
||||
minSize={25}
|
||||
collapsedSize={isHorizontalLayout ? 20 : 4}
|
||||
className="p-2 flex flex-col"
|
||||
collapsible
|
||||
onCollapse={() => setIsPreviewCollapsed(true)}
|
||||
onExpand={() => setIsPreviewCollapsed(false)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
onClick={toggleLayout}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="mr-2 border"
|
||||
disabled={isAIChatOpen}
|
||||
>
|
||||
{isHorizontalLayout ? <ArrowRightToLine className="w-4 h-4" /> : <ArrowDownToLine className="w-4 h-4" />}
|
||||
</Button>
|
||||
<PreviewWindow
|
||||
open={togglePreviewPanel}
|
||||
collapsed={isPreviewCollapsed}
|
||||
src={previewURL}
|
||||
ref={previewWindowRef}
|
||||
/>
|
||||
</div>
|
||||
{!isPreviewCollapsed && (
|
||||
<div className="w-full grow rounded-md overflow-hidden bg-foreground mt-2">
|
||||
<iframe
|
||||
width={"100%"}
|
||||
height={"100%"}
|
||||
src={previewURL}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ResizablePanel>
|
||||
<ResizableHandle />
|
||||
<ResizablePanel
|
||||
defaultSize={50}
|
||||
minSize={20}
|
||||
className="p-2 flex flex-col"
|
||||
>
|
||||
{isOwner ? (
|
||||
<Terminals />
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-lg font-medium text-muted-foreground/50 select-none">
|
||||
<TerminalSquare className="w-4 h-4 mr-2" />
|
||||
No terminal access.
|
||||
</div>
|
||||
)}
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
<ResizablePanel
|
||||
defaultSize={50}
|
||||
minSize={20}
|
||||
className="p-2 flex flex-col"
|
||||
>
|
||||
{isOwner ? (
|
||||
<Terminals />
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-lg font-medium text-muted-foreground/50 select-none">
|
||||
<TerminalSquare className="w-4 h-4 mr-2" />
|
||||
No terminal access.
|
||||
</div>
|
||||
)}
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</ResizablePanel>
|
||||
{/* Right side: AIChat (if open) */}
|
||||
{isAIChatOpen && (
|
||||
<>
|
||||
<ResizableHandle />
|
||||
<ResizablePanel defaultSize={30} minSize={15}>
|
||||
<AIChat
|
||||
activeFileContent={activeFileContent}
|
||||
activeFileName={tabs.find(tab => tab.id === activeFileId)?.name || 'No file selected'}
|
||||
onClose={toggleAIChat}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</>
|
||||
)}
|
||||
</ResizablePanelGroup>
|
||||
</PreviewProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the typescript compiler to detect JSX and load type definitions
|
||||
*/
|
||||
const defaultCompilerOptions: monaco.languages.typescript.CompilerOptions = {
|
||||
allowJs: true,
|
||||
allowSyntheticDefaultImports: true,
|
||||
allowNonTsExtensions: true,
|
||||
resolveJsonModule: true,
|
||||
|
||||
jsx: monaco.languages.typescript.JsxEmit.ReactJSX,
|
||||
module: monaco.languages.typescript.ModuleKind.ESNext,
|
||||
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||
target: monaco.languages.typescript.ScriptTarget.ESNext,
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import {
|
||||
Link,
|
||||
RotateCw,
|
||||
TerminalSquare,
|
||||
UnfoldVertical,
|
||||
} from "lucide-react"
|
||||
import { useEffect, useRef, useState, useImperativeHandle, forwardRef } from "react"
|
||||
import { toast } from "sonner"
|
||||
@ -33,18 +32,24 @@ ref: React.Ref<{
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`${collapsed ? "h-full" : "h-10"
|
||||
} select-none w-full flex gap-2`}
|
||||
>
|
||||
<div className="h-8 rounded-md px-3 bg-secondary flex items-center w-full justify-between">
|
||||
<div className="text-xs">Preview</div>
|
||||
<div className="flex space-x-1 translate-x-1">
|
||||
{collapsed ? (
|
||||
<PreviewButton onClick={open}>
|
||||
<UnfoldVertical className="w-4 h-4" />
|
||||
<PreviewButton disabled onClick={() => { }}>
|
||||
<TerminalSquare className="w-4 h-4" />
|
||||
</PreviewButton>
|
||||
) : (
|
||||
<>
|
||||
{/* Removed the unfoldvertical button since we have the same thing via the run button.
|
||||
|
||||
<PreviewButton onClick={open}>
|
||||
<UnfoldVertical className="w-4 h-4" />
|
||||
</PreviewButton>
|
||||
</PreviewButton> */}
|
||||
|
||||
<PreviewButton
|
||||
onClick={() => {
|
||||
@ -61,6 +66,18 @@ ref: React.Ref<{
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{collapsed ? null : (
|
||||
<div className="w-full grow rounded-md overflow-hidden bg-foreground">
|
||||
<iframe
|
||||
key={iframeKey}
|
||||
ref={frameRef}
|
||||
width={"100%"}
|
||||
height={"100%"}
|
||||
src={src}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
@ -90,9 +90,9 @@ export default function SidebarFile({
|
||||
if (!editing && !pendingDelete && !isMoving)
|
||||
selectFile({ ...data, saved: true });
|
||||
}}
|
||||
onDoubleClick={() => {
|
||||
setEditing(true)
|
||||
}}
|
||||
// onDoubleClick={() => {
|
||||
// setEditing(true)
|
||||
// }}
|
||||
className={`${
|
||||
dragging ? "opacity-50 hover:!bg-background" : ""
|
||||
} data-[state=open]:bg-secondary/50 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`}
|
||||
|
@ -1,20 +1,18 @@
|
||||
"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 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 {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuTrigger,
|
||||
} from "@/components/ui/context-menu"
|
||||
import { ChevronRight, Loader2, Pencil, Trash2 } from "lucide-react"
|
||||
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
} from "@/components/ui/context-menu";
|
||||
import { Loader2, Pencil, Trash2 } from "lucide-react";
|
||||
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
||||
|
||||
// Note: Renaming has not been implemented in the backend yet, so UI relating to renaming is commented out
|
||||
|
||||
@ -27,27 +25,27 @@ export default function SidebarFolder({
|
||||
movingId,
|
||||
deletingFolderId,
|
||||
}: {
|
||||
data: TFolder
|
||||
selectFile: (file: TTab) => void
|
||||
data: TFolder;
|
||||
selectFile: (file: TTab) => void;
|
||||
handleRename: (
|
||||
id: string,
|
||||
newName: string,
|
||||
oldName: string,
|
||||
type: "file" | "folder"
|
||||
) => boolean
|
||||
handleDeleteFile: (file: TFile) => void
|
||||
handleDeleteFolder: (folder: TFolder) => void
|
||||
movingId: string
|
||||
deletingFolderId: string
|
||||
) => boolean;
|
||||
handleDeleteFile: (file: TFile) => void;
|
||||
handleDeleteFolder: (folder: TFolder) => void;
|
||||
movingId: string;
|
||||
deletingFolderId: string;
|
||||
}) {
|
||||
const ref = useRef(null) // drop target
|
||||
const [isDraggedOver, setIsDraggedOver] = useState(false)
|
||||
const ref = useRef(null); // drop target
|
||||
const [isDraggedOver, setIsDraggedOver] = useState(false);
|
||||
|
||||
const isDeleting =
|
||||
deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId)
|
||||
deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current
|
||||
const el = ref.current;
|
||||
|
||||
if (el)
|
||||
return dropTargetForElements({
|
||||
@ -69,17 +67,17 @@ export default function SidebarFolder({
|
||||
|
||||
// no dropping while awaiting move
|
||||
canDrop: () => {
|
||||
return !movingId
|
||||
return !movingId;
|
||||
},
|
||||
})
|
||||
}, [])
|
||||
});
|
||||
}, []);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const folder = isOpen
|
||||
? getIconForOpenFolder(data.name)
|
||||
: getIconForFolder(data.name)
|
||||
: getIconForFolder(data.name);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
// const [editing, setEditing] = useState(false);
|
||||
|
||||
// useEffect(() => {
|
||||
@ -98,12 +96,6 @@ export default function SidebarFolder({
|
||||
isDraggedOver ? "bg-secondary/50 rounded-t-sm" : "rounded-sm"
|
||||
} w-full flex items-center h-7 px-1 transition-colors hover:bg-secondary cursor-pointer`}
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
"min-w-3 min-h-3 mr-1 ml-auto transition-all duration-300",
|
||||
isOpen ? "transform rotate-90" : ""
|
||||
)}
|
||||
/>
|
||||
<Image
|
||||
src={`/icons/${folder}`}
|
||||
alt="Folder icon"
|
||||
@ -157,65 +149,48 @@ export default function SidebarFolder({
|
||||
<ContextMenuItem
|
||||
disabled={isDeleting}
|
||||
onClick={() => {
|
||||
handleDeleteFolder(data)
|
||||
handleDeleteFolder(data);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
Delete
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
<AnimatePresence>
|
||||
{isOpen ? (
|
||||
<motion.div
|
||||
className="overflow-y-hidden"
|
||||
initial={{
|
||||
height: 0,
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
height: "auto",
|
||||
opacity: 1,
|
||||
}}
|
||||
exit={{
|
||||
height: 0,
|
||||
opacity: 0,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
isDraggedOver ? "rounded-b-sm bg-secondary/50" : ""
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col grow ml-2 pl-2 border-l border-border">
|
||||
{data.children.map((child) =>
|
||||
child.type === "file" ? (
|
||||
<SidebarFile
|
||||
key={child.id}
|
||||
data={child}
|
||||
selectFile={selectFile}
|
||||
handleRename={handleRename}
|
||||
handleDeleteFile={handleDeleteFile}
|
||||
movingId={movingId}
|
||||
deletingFolderId={deletingFolderId}
|
||||
/>
|
||||
) : (
|
||||
<SidebarFolder
|
||||
key={child.id}
|
||||
data={child}
|
||||
selectFile={selectFile}
|
||||
handleRename={handleRename}
|
||||
handleDeleteFile={handleDeleteFile}
|
||||
handleDeleteFolder={handleDeleteFolder}
|
||||
movingId={movingId}
|
||||
deletingFolderId={deletingFolderId}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
{isOpen ? (
|
||||
<div
|
||||
className={`flex w-full items-stretch ${
|
||||
isDraggedOver ? "rounded-b-sm bg-secondary/50" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="w-[1px] bg-border mx-2 h-full"></div>
|
||||
<div className="flex flex-col grow">
|
||||
{data.children.map((child) =>
|
||||
child.type === "file" ? (
|
||||
<SidebarFile
|
||||
key={child.id}
|
||||
data={child}
|
||||
selectFile={selectFile}
|
||||
handleRename={handleRename}
|
||||
handleDeleteFile={handleDeleteFile}
|
||||
movingId={movingId}
|
||||
deletingFolderId={deletingFolderId}
|
||||
/>
|
||||
) : (
|
||||
<SidebarFolder
|
||||
key={child.id}
|
||||
data={child}
|
||||
selectFile={selectFile}
|
||||
handleRename={handleRename}
|
||||
handleDeleteFile={handleDeleteFile}
|
||||
handleDeleteFolder={handleDeleteFolder}
|
||||
movingId={movingId}
|
||||
deletingFolderId={deletingFolderId}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</ContextMenu>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -4,8 +4,9 @@ import {
|
||||
FilePlus,
|
||||
FolderPlus,
|
||||
Loader2,
|
||||
MonitorPlay,
|
||||
Search,
|
||||
Sparkles,
|
||||
MessageSquareMore,
|
||||
} from "lucide-react";
|
||||
import SidebarFile from "./file";
|
||||
import SidebarFolder from "./folder";
|
||||
@ -13,13 +14,14 @@ import { Sandbox, TFile, TFolder, TTab } from "@/lib/types";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import New from "./new";
|
||||
import { Socket } from "socket.io-client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
import {
|
||||
dropTargetForElements,
|
||||
monitorForElements,
|
||||
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
||||
|
||||
import Button from "@/components/ui/customButton";
|
||||
|
||||
export default function Sidebar({
|
||||
sandboxData,
|
||||
files,
|
||||
@ -103,9 +105,9 @@ export default function Sidebar({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-full w-56 select-none flex flex-col text-sm">
|
||||
<div className="flex-grow overflow-auto p-2 pb-[84px]">
|
||||
<div className="flex w-full items-center justify-between h-8 mb-1">
|
||||
<div className="h-full w-56 select-none flex flex-col text-sm items-start justify-between p-2">
|
||||
<div className="w-full flex flex-col items-start">
|
||||
<div className="flex w-full items-center justify-between h-8 mb-1 ">
|
||||
<div className="text-muted-foreground">Explorer</div>
|
||||
<div className="flex space-x-1">
|
||||
<button
|
||||
@ -179,25 +181,10 @@ export default function Sidebar({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="fixed bottom-0 w-48 flex flex-col p-2 bg-background">
|
||||
<Button variant="ghost" className="w-full justify-start text-sm text-muted-foreground font-normal h-8 px-2 mb-2" disabled aria-disabled="true" style={{ opacity: 1}}>
|
||||
<Sparkles className="h-4 w-4 mr-2 text-indigo-500 opacity-70" />
|
||||
Copilot
|
||||
<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="w-full justify-start text-sm text-muted-foreground font-normal h-8 px-2 mb-2" disabled aria-disabled="true" style={{ opacity: 1 }}>
|
||||
<MessageSquareMore className="h-4 w-4 mr-2 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 className="w-full space-y-4">
|
||||
{/* <Button className="w-full">
|
||||
<MonitorPlay className="w-4 h-4 mr-2" /> Run
|
||||
</Button> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,32 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const LoadingDots: React.FC = () => {
|
||||
return (
|
||||
<span className="loading-dots">
|
||||
<span className="dot">.</span>
|
||||
<span className="dot">.</span>
|
||||
<span className="dot">.</span>
|
||||
<style jsx>{`
|
||||
.loading-dots {
|
||||
display: inline-block;
|
||||
font-size: 24px;
|
||||
}
|
||||
.dot {
|
||||
opacity: 0;
|
||||
animation: showHideDot 1.5s ease-in-out infinite;
|
||||
}
|
||||
.dot:nth-child(1) { animation-delay: 0s; }
|
||||
.dot:nth-child(2) { animation-delay: 0.5s; }
|
||||
.dot:nth-child(3) { animation-delay: 1s; }
|
||||
@keyframes showHideDot {
|
||||
0% { opacity: 0; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
`}</style>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingDots;
|
||||
|
@ -1,36 +0,0 @@
|
||||
export const projectTemplates: {
|
||||
id: string
|
||||
name: string
|
||||
icon: string
|
||||
description: string
|
||||
disabled: boolean
|
||||
}[] = [
|
||||
{
|
||||
id: "reactjs",
|
||||
name: "React",
|
||||
icon: "/project-icons/react.svg",
|
||||
description: "A JavaScript library for building user interfaces",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
id: "vanillajs",
|
||||
name: "HTML/JS",
|
||||
icon: "/project-icons/more.svg",
|
||||
description: "More coming soon, feel free to contribute on GitHub",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
id: "nextjs",
|
||||
name: "NextJS",
|
||||
icon: "/project-icons/node.svg",
|
||||
description: "A JavaScript runtime built on the V8 JavaScript engine",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
id: "streamlit",
|
||||
name: "Streamlit",
|
||||
icon: "/project-icons/python.svg",
|
||||
description: "A JavaScript runtime built on the V8 JavaScript engine",
|
||||
disabled: false,
|
||||
},
|
||||
]
|
@ -1,99 +0,0 @@
|
||||
import * as monaco from "monaco-editor"
|
||||
|
||||
export function parseTSConfigToMonacoOptions(
|
||||
tsconfig: any
|
||||
): monaco.languages.typescript.CompilerOptions {
|
||||
const compilerOptions: monaco.languages.typescript.CompilerOptions = {}
|
||||
|
||||
// Map tsconfig options to Monaco CompilerOptions
|
||||
if (tsconfig.strict) compilerOptions.strict = tsconfig.strict
|
||||
if (tsconfig.target) compilerOptions.target = mapScriptTarget(tsconfig.target)
|
||||
if (tsconfig.module) compilerOptions.module = mapModule(tsconfig.module)
|
||||
if (tsconfig.lib) compilerOptions.lib = tsconfig.lib
|
||||
if (tsconfig.allowJs) compilerOptions.allowJs = tsconfig.allowJs
|
||||
if (tsconfig.checkJs) compilerOptions.checkJs = tsconfig.checkJs
|
||||
if (tsconfig.jsx) compilerOptions.jsx = mapJSX(tsconfig.jsx)
|
||||
if (tsconfig.declaration) compilerOptions.declaration = tsconfig.declaration
|
||||
if (tsconfig.declarationMap)
|
||||
compilerOptions.declarationMap = tsconfig.declarationMap
|
||||
if (tsconfig.sourceMap) compilerOptions.sourceMap = tsconfig.sourceMap
|
||||
if (tsconfig.outFile) compilerOptions.outFile = tsconfig.outFile
|
||||
if (tsconfig.outDir) compilerOptions.outDir = tsconfig.outDir
|
||||
if (tsconfig.removeComments)
|
||||
compilerOptions.removeComments = tsconfig.removeComments
|
||||
if (tsconfig.noEmit) compilerOptions.noEmit = tsconfig.noEmit
|
||||
if (tsconfig.noEmitOnError)
|
||||
compilerOptions.noEmitOnError = tsconfig.noEmitOnError
|
||||
|
||||
return compilerOptions
|
||||
}
|
||||
|
||||
function mapScriptTarget(
|
||||
target: string
|
||||
): monaco.languages.typescript.ScriptTarget {
|
||||
const targetMap: { [key: string]: monaco.languages.typescript.ScriptTarget } =
|
||||
{
|
||||
es3: monaco.languages.typescript.ScriptTarget.ES3,
|
||||
es5: monaco.languages.typescript.ScriptTarget.ES5,
|
||||
es6: monaco.languages.typescript.ScriptTarget.ES2015,
|
||||
es2015: monaco.languages.typescript.ScriptTarget.ES2015,
|
||||
es2016: monaco.languages.typescript.ScriptTarget.ES2016,
|
||||
es2017: monaco.languages.typescript.ScriptTarget.ES2017,
|
||||
es2018: monaco.languages.typescript.ScriptTarget.ES2018,
|
||||
es2019: monaco.languages.typescript.ScriptTarget.ES2019,
|
||||
es2020: monaco.languages.typescript.ScriptTarget.ES2020,
|
||||
esnext: monaco.languages.typescript.ScriptTarget.ESNext,
|
||||
}
|
||||
if (typeof target !== "string") {
|
||||
return monaco.languages.typescript.ScriptTarget.Latest
|
||||
}
|
||||
return (
|
||||
targetMap[target?.toLowerCase()] ||
|
||||
monaco.languages.typescript.ScriptTarget.Latest
|
||||
)
|
||||
}
|
||||
|
||||
function mapModule(module: string): monaco.languages.typescript.ModuleKind {
|
||||
const moduleMap: { [key: string]: monaco.languages.typescript.ModuleKind } = {
|
||||
none: monaco.languages.typescript.ModuleKind.None,
|
||||
commonjs: monaco.languages.typescript.ModuleKind.CommonJS,
|
||||
amd: monaco.languages.typescript.ModuleKind.AMD,
|
||||
umd: monaco.languages.typescript.ModuleKind.UMD,
|
||||
system: monaco.languages.typescript.ModuleKind.System,
|
||||
es6: monaco.languages.typescript.ModuleKind.ES2015,
|
||||
es2015: monaco.languages.typescript.ModuleKind.ES2015,
|
||||
esnext: monaco.languages.typescript.ModuleKind.ESNext,
|
||||
}
|
||||
if (typeof module !== "string") {
|
||||
return monaco.languages.typescript.ModuleKind.ESNext
|
||||
}
|
||||
return (
|
||||
moduleMap[module.toLowerCase()] ||
|
||||
monaco.languages.typescript.ModuleKind.ESNext
|
||||
)
|
||||
}
|
||||
|
||||
function mapJSX(jsx: string): monaco.languages.typescript.JsxEmit {
|
||||
const jsxMap: { [key: string]: monaco.languages.typescript.JsxEmit } = {
|
||||
preserve: monaco.languages.typescript.JsxEmit.Preserve,
|
||||
react: monaco.languages.typescript.JsxEmit.React,
|
||||
"react-native": monaco.languages.typescript.JsxEmit.ReactNative,
|
||||
}
|
||||
return jsxMap[jsx.toLowerCase()] || monaco.languages.typescript.JsxEmit.React
|
||||
}
|
||||
|
||||
// Example usage:
|
||||
const tsconfigJSON = {
|
||||
compilerOptions: {
|
||||
strict: true,
|
||||
target: "ES2020",
|
||||
module: "ESNext",
|
||||
lib: ["DOM", "ES2020"],
|
||||
jsx: "react",
|
||||
sourceMap: true,
|
||||
outDir: "./dist",
|
||||
},
|
||||
}
|
||||
|
||||
const monacoOptions = parseTSConfigToMonacoOptions(tsconfigJSON.compilerOptions)
|
||||
console.log(monacoOptions)
|
@ -75,26 +75,3 @@ export function debounce<T extends (...args: any[]) => void>(
|
||||
timeout = setTimeout(() => func(...args), wait)
|
||||
} as T
|
||||
}
|
||||
|
||||
// Deep merge utility function
|
||||
export const deepMerge = (target: any, source: any) => {
|
||||
const output = { ...target }
|
||||
if (isObject(target) && isObject(source)) {
|
||||
Object.keys(source).forEach((key) => {
|
||||
if (isObject(source[key])) {
|
||||
if (!(key in target)) {
|
||||
Object.assign(output, { [key]: source[key] })
|
||||
} else {
|
||||
output[key] = deepMerge(target[key], source[key])
|
||||
}
|
||||
} else {
|
||||
Object.assign(output, { [key]: source[key] })
|
||||
}
|
||||
})
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
const isObject = (item: any) => {
|
||||
return item && typeof item === "object" && !Array.isArray(item)
|
||||
}
|
||||
|
2012
frontend/package-lock.json
generated
2012
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -12,7 +12,6 @@
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.1.7",
|
||||
"@clerk/nextjs": "^4.29.12",
|
||||
"@clerk/themes": "^1.7.12",
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@liveblocks/client": "^1.12.0",
|
||||
"@liveblocks/node": "^1.12.0",
|
||||
@ -31,8 +30,6 @@
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@react-three/fiber": "^8.16.6",
|
||||
"@uiw/codemirror-theme-vscode": "^4.23.5",
|
||||
"@uiw/react-codemirror": "^4.23.5",
|
||||
"@vercel/analytics": "^1.2.2",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
@ -49,10 +46,7 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.51.3",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-resizable-panels": "^2.0.16",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"sonner": "^1.4.41",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
@ -65,11 +59,9 @@
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/estree": "^1.0.6",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/three": "^0.164.0",
|
||||
"autoprefixer": "^10.0.1",
|
||||
"postcss": "^8",
|
||||
|
3
frontend/react-syntax-highlighter.d.ts
vendored
3
frontend/react-syntax-highlighter.d.ts
vendored
@ -1,3 +0,0 @@
|
||||
declare module 'react-syntax-highlighter';
|
||||
declare module 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
|
@ -1,7 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"types": ["node"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
|
Reference in New Issue
Block a user