input channel mapping update
This commit is contained in:
@@ -1,31 +1,31 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import Editor from "@monaco-editor/react";
|
import Editor from "@monaco-editor/react";
|
||||||
import {
|
import {
|
||||||
X, Plus, Save, FileCode, FileText, Folder, ChevronDown, ChevronRight,
|
X, Plus, Save, FileCode, FileText, Folder, ChevronDown, ChevronRight,
|
||||||
File, FilePlus, FolderPlus, Trash2, Edit, MoreHorizontal, Play,
|
File, FilePlus, FolderPlus, Trash2, Edit, MoreHorizontal, Play,
|
||||||
Terminal, Loader
|
Terminal, Loader
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Sidebar from "./Sidebar";
|
import Sidebar from "./Sidebar";
|
||||||
import Panel from "./Panel"; // Import Panel component
|
import Panel from "./Panel"; // Import Panel component
|
||||||
|
|
||||||
const EditorArea = ({
|
const EditorArea = ({
|
||||||
sidebarVisible = true,
|
sidebarVisible = true,
|
||||||
activeView = "explorer",
|
activeView = "explorer",
|
||||||
panelVisible,
|
panelVisible,
|
||||||
setPanelVisible
|
setPanelVisible
|
||||||
}) => {
|
}) => {
|
||||||
// Store files with their content in state - start with just README.md
|
// Store files with their content in state - start with just README.md
|
||||||
const [files, setFiles] = useState([
|
const [files, setFiles] = useState([
|
||||||
{ id: "README.md", language: "markdown", content: getDefaultCode("README.md") },
|
{ id: "README.md", language: "markdown", content: getDefaultCode("README.md") },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState(files[0]?.id || "");
|
const [activeTab, setActiveTab] = useState(files[0]?.id || "");
|
||||||
const [isNewFileModalOpen, setIsNewFileModalOpen] = useState(false);
|
const [isNewFileModalOpen, setIsNewFileModalOpen] = useState(false);
|
||||||
const [newFileName, setNewFileName] = useState("");
|
const [newFileName, setNewFileName] = useState("");
|
||||||
const [newFileType, setNewFileType] = useState("javascript");
|
const [newFileType, setNewFileType] = useState("javascript");
|
||||||
const [unsavedChanges, setUnsavedChanges] = useState({});
|
const [unsavedChanges, setUnsavedChanges] = useState({});
|
||||||
|
|
||||||
// Sidebar state - now receives visibility from props
|
// Sidebar state - now receives visibility from props
|
||||||
const [sidebarWidth, setSidebarWidth] = useState(250);
|
const [sidebarWidth, setSidebarWidth] = useState(250);
|
||||||
const [expandedFolders, setExpandedFolders] = useState({});
|
const [expandedFolders, setExpandedFolders] = useState({});
|
||||||
@@ -45,7 +45,7 @@ const EditorArea = ({
|
|||||||
const [showContextMenu, setShowContextMenu] = useState(false);
|
const [showContextMenu, setShowContextMenu] = useState(false);
|
||||||
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
|
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
|
||||||
const [contextMenuTarget, setContextMenuTarget] = useState(null);
|
const [contextMenuTarget, setContextMenuTarget] = useState(null);
|
||||||
|
|
||||||
const editorRef = useRef(null);
|
const editorRef = useRef(null);
|
||||||
const newFileInputRef = useRef(null);
|
const newFileInputRef = useRef(null);
|
||||||
const renameInputRef = useRef(null);
|
const renameInputRef = useRef(null);
|
||||||
@@ -83,7 +83,7 @@ const EditorArea = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedFiles = localStorage.getItem("vscode-clone-files");
|
const savedFiles = localStorage.getItem("vscode-clone-files");
|
||||||
const savedFileStructure = localStorage.getItem("vscode-clone-structure");
|
const savedFileStructure = localStorage.getItem("vscode-clone-structure");
|
||||||
|
|
||||||
if (savedFiles) {
|
if (savedFiles) {
|
||||||
try {
|
try {
|
||||||
const parsedFiles = JSON.parse(savedFiles);
|
const parsedFiles = JSON.parse(savedFiles);
|
||||||
@@ -95,7 +95,7 @@ const EditorArea = ({
|
|||||||
console.error("Failed to load saved files:", error);
|
console.error("Failed to load saved files:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (savedFileStructure) {
|
if (savedFileStructure) {
|
||||||
try {
|
try {
|
||||||
const parsedStructure = JSON.parse(savedFileStructure);
|
const parsedStructure = JSON.parse(savedFileStructure);
|
||||||
@@ -110,7 +110,7 @@ const EditorArea = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem("vscode-clone-files", JSON.stringify(files));
|
localStorage.setItem("vscode-clone-files", JSON.stringify(files));
|
||||||
}, [files]);
|
}, [files]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem("vscode-clone-structure", JSON.stringify(fileStructure));
|
localStorage.setItem("vscode-clone-structure", JSON.stringify(fileStructure));
|
||||||
}, [fileStructure]);
|
}, [fileStructure]);
|
||||||
@@ -142,31 +142,31 @@ const EditorArea = ({
|
|||||||
...prev,
|
...prev,
|
||||||
[activeTab]: true
|
[activeTab]: true
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Update the file content in the files array
|
// Update the file content in the files array
|
||||||
setFiles(files.map(file =>
|
setFiles(files.map(file =>
|
||||||
file.id === activeTab ? { ...file, content: value } : file
|
file.id === activeTab ? { ...file, content: value } : file
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseTab = (e, fileId) => {
|
const handleCloseTab = (e, fileId) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
if (unsavedChanges[fileId]) {
|
if (unsavedChanges[fileId]) {
|
||||||
if (!confirm(`You have unsaved changes in ${fileId}. Close anyway?`)) {
|
if (!confirm(`You have unsaved changes in ${fileId}. Close anyway?`)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the file from the files array
|
// Remove the file from the files array
|
||||||
const newFiles = files.filter(file => file.id !== fileId);
|
const newFiles = files.filter(file => file.id !== fileId);
|
||||||
setFiles(newFiles);
|
setFiles(newFiles);
|
||||||
|
|
||||||
// Update unsavedChanges
|
// Update unsavedChanges
|
||||||
const newUnsavedChanges = { ...unsavedChanges };
|
const newUnsavedChanges = { ...unsavedChanges };
|
||||||
delete newUnsavedChanges[fileId];
|
delete newUnsavedChanges[fileId];
|
||||||
setUnsavedChanges(newUnsavedChanges);
|
setUnsavedChanges(newUnsavedChanges);
|
||||||
|
|
||||||
// If the active tab is closed, set a new active tab
|
// If the active tab is closed, set a new active tab
|
||||||
if (activeTab === fileId && newFiles.length > 0) {
|
if (activeTab === fileId && newFiles.length > 0) {
|
||||||
setActiveTab(newFiles[0].id);
|
setActiveTab(newFiles[0].id);
|
||||||
@@ -175,17 +175,17 @@ const EditorArea = ({
|
|||||||
|
|
||||||
const handleCreateNewFile = (e, path = '') => {
|
const handleCreateNewFile = (e, path = '') => {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
|
|
||||||
if (!newFileName) return;
|
if (!newFileName) return;
|
||||||
|
|
||||||
const filePath = path ? `${path}/${newFileName}` : newFileName;
|
const filePath = path ? `${path}/${newFileName}` : newFileName;
|
||||||
|
|
||||||
// Check if file already exists
|
// Check if file already exists
|
||||||
if (files.some(file => file.id === filePath)) {
|
if (files.some(file => file.id === filePath)) {
|
||||||
alert(`A file named "${filePath}" already exists.`);
|
alert(`A file named "${filePath}" already exists.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine language based on file extension
|
// Determine language based on file extension
|
||||||
let language = newFileType;
|
let language = newFileType;
|
||||||
const extension = newFileName.split('.').pop().toLowerCase();
|
const extension = newFileName.split('.').pop().toLowerCase();
|
||||||
@@ -200,20 +200,20 @@ const EditorArea = ({
|
|||||||
} else if (['md', 'markdown'].includes(extension)) {
|
} else if (['md', 'markdown'].includes(extension)) {
|
||||||
language = 'markdown';
|
language = 'markdown';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new file
|
// Create new file
|
||||||
const newFile = {
|
const newFile = {
|
||||||
id: filePath,
|
id: filePath,
|
||||||
language,
|
language,
|
||||||
content: ''
|
content: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
setFiles([...files, newFile]);
|
setFiles([...files, newFile]);
|
||||||
setActiveTab(filePath);
|
setActiveTab(filePath);
|
||||||
|
|
||||||
// Update file structure
|
// Update file structure
|
||||||
updateFileStructure(filePath, 'file', language);
|
updateFileStructure(filePath, 'file', language);
|
||||||
|
|
||||||
setNewFileName('');
|
setNewFileName('');
|
||||||
setIsNewFileModalOpen(false);
|
setIsNewFileModalOpen(false);
|
||||||
};
|
};
|
||||||
@@ -222,7 +222,7 @@ const EditorArea = ({
|
|||||||
const parts = path.split('/');
|
const parts = path.split('/');
|
||||||
const fileName = parts.pop();
|
const fileName = parts.pop();
|
||||||
let current = fileStructure;
|
let current = fileStructure;
|
||||||
|
|
||||||
// Navigate to the correct folder
|
// Navigate to the correct folder
|
||||||
if (parts.length > 0) {
|
if (parts.length > 0) {
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
@@ -232,14 +232,14 @@ const EditorArea = ({
|
|||||||
current = current[part].children;
|
current = current[part].children;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the new item to the structure
|
// Add the new item to the structure
|
||||||
if (type === 'file') {
|
if (type === 'file') {
|
||||||
current[fileName] = { type: 'file', language, id: path };
|
current[fileName] = { type: 'file', language, id: path };
|
||||||
} else if (type === 'folder') {
|
} else if (type === 'folder') {
|
||||||
current[fileName] = { type: 'folder', children: {} };
|
current[fileName] = { type: 'folder', children: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the state with the new structure
|
// Update the state with the new structure
|
||||||
setFileStructure({...fileStructure});
|
setFileStructure({...fileStructure});
|
||||||
};
|
};
|
||||||
@@ -247,10 +247,10 @@ const EditorArea = ({
|
|||||||
const createNewFolder = (path = '') => {
|
const createNewFolder = (path = '') => {
|
||||||
const folderName = prompt("Enter folder name:");
|
const folderName = prompt("Enter folder name:");
|
||||||
if (!folderName) return;
|
if (!folderName) return;
|
||||||
|
|
||||||
const folderPath = path ? `${path}/${folderName}` : folderName;
|
const folderPath = path ? `${path}/${folderName}` : folderName;
|
||||||
updateFileStructure(folderPath, 'folder');
|
updateFileStructure(folderPath, 'folder');
|
||||||
|
|
||||||
// If the folder is inside another folder, expand the parent
|
// If the folder is inside another folder, expand the parent
|
||||||
if (path) {
|
if (path) {
|
||||||
setExpandedFolders({
|
setExpandedFolders({
|
||||||
@@ -266,7 +266,7 @@ const EditorArea = ({
|
|||||||
...prev,
|
...prev,
|
||||||
[activeTab]: false
|
[activeTab]: false
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// In a real app, you would save to the server here
|
// In a real app, you would save to the server here
|
||||||
console.log(`File ${activeTab} saved!`);
|
console.log(`File ${activeTab} saved!`);
|
||||||
};
|
};
|
||||||
@@ -281,14 +281,14 @@ const EditorArea = ({
|
|||||||
const openFile = (fileId) => {
|
const openFile = (fileId) => {
|
||||||
// Check if file exists in files array
|
// Check if file exists in files array
|
||||||
const fileExists = files.some(file => file.id === fileId);
|
const fileExists = files.some(file => file.id === fileId);
|
||||||
|
|
||||||
if (!fileExists) {
|
if (!fileExists) {
|
||||||
// Determine language from file structure
|
// Determine language from file structure
|
||||||
let language = 'text';
|
let language = 'text';
|
||||||
const parts = fileId.split('/');
|
const parts = fileId.split('/');
|
||||||
const fileName = parts.pop();
|
const fileName = parts.pop();
|
||||||
const extension = fileName.split('.').pop().toLowerCase();
|
const extension = fileName.split('.').pop().toLowerCase();
|
||||||
|
|
||||||
if (['jsx', 'js', 'ts', 'tsx'].includes(extension)) {
|
if (['jsx', 'js', 'ts', 'tsx'].includes(extension)) {
|
||||||
language = 'javascript';
|
language = 'javascript';
|
||||||
} else if (['css', 'scss', 'less'].includes(extension)) {
|
} else if (['css', 'scss', 'less'].includes(extension)) {
|
||||||
@@ -300,17 +300,17 @@ const EditorArea = ({
|
|||||||
} else if (['md', 'markdown'].includes(extension)) {
|
} else if (['md', 'markdown'].includes(extension)) {
|
||||||
language = 'markdown';
|
language = 'markdown';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new file entry
|
// Create new file entry
|
||||||
const newFile = {
|
const newFile = {
|
||||||
id: fileId,
|
id: fileId,
|
||||||
language,
|
language,
|
||||||
content: ''
|
content: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
setFiles([...files, newFile]);
|
setFiles([...files, newFile]);
|
||||||
}
|
}
|
||||||
|
|
||||||
setActiveTab(fileId);
|
setActiveTab(fileId);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -329,82 +329,82 @@ const EditorArea = ({
|
|||||||
const deleteItem = (path, type) => {
|
const deleteItem = (path, type) => {
|
||||||
const confirmDelete = confirm(`Are you sure you want to delete ${path}?`);
|
const confirmDelete = confirm(`Are you sure you want to delete ${path}?`);
|
||||||
if (!confirmDelete) return;
|
if (!confirmDelete) return;
|
||||||
|
|
||||||
if (type === 'file') {
|
if (type === 'file') {
|
||||||
// Remove from files array
|
// Remove from files array
|
||||||
setFiles(files.filter(file => file.id !== path));
|
setFiles(files.filter(file => file.id !== path));
|
||||||
|
|
||||||
// If it was active, set a new active tab
|
// If it was active, set a new active tab
|
||||||
if (activeTab === path) {
|
if (activeTab === path) {
|
||||||
const newActiveTab = files.find(file => file.id !== path)?.id || '';
|
const newActiveTab = files.find(file => file.id !== path)?.id || '';
|
||||||
setActiveTab(newActiveTab);
|
setActiveTab(newActiveTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from unsavedChanges
|
// Remove from unsavedChanges
|
||||||
const newUnsavedChanges = { ...unsavedChanges };
|
const newUnsavedChanges = { ...unsavedChanges };
|
||||||
delete newUnsavedChanges[path];
|
delete newUnsavedChanges[path];
|
||||||
setUnsavedChanges(newUnsavedChanges);
|
setUnsavedChanges(newUnsavedChanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from file structure
|
// Remove from file structure
|
||||||
const parts = path.split('/');
|
const parts = path.split('/');
|
||||||
const itemName = parts.pop();
|
const itemName = parts.pop();
|
||||||
let current = fileStructure;
|
let current = fileStructure;
|
||||||
let parent = null;
|
let parent = null;
|
||||||
|
|
||||||
// Navigate to the correct folder
|
// Navigate to the correct folder
|
||||||
if (parts.length > 0) {
|
if (parts.length > 0) {
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
parent = current;
|
parent = current;
|
||||||
current = current[part].children;
|
current = current[part].children;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the item
|
// Delete the item
|
||||||
delete current[itemName];
|
delete current[itemName];
|
||||||
} else {
|
} else {
|
||||||
// Delete top-level item
|
// Delete top-level item
|
||||||
delete fileStructure[itemName];
|
delete fileStructure[itemName];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the state
|
// Update the state
|
||||||
setFileStructure({...fileStructure});
|
setFileStructure({...fileStructure});
|
||||||
};
|
};
|
||||||
|
|
||||||
const startRenaming = (path, type) => {
|
const startRenaming = (path, type) => {
|
||||||
setRenamePath(path);
|
setRenamePath(path);
|
||||||
|
|
||||||
const parts = path.split('/');
|
const parts = path.split('/');
|
||||||
const currentName = parts.pop();
|
const currentName = parts.pop();
|
||||||
setRenameValue(currentName);
|
setRenameValue(currentName);
|
||||||
|
|
||||||
setIsRenaming(true);
|
setIsRenaming(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRename = (e) => {
|
const handleRename = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!renameValue.trim()) return;
|
if (!renameValue.trim()) return;
|
||||||
|
|
||||||
const parts = renamePath.split('/');
|
const parts = renamePath.split('/');
|
||||||
const oldName = parts.pop();
|
const oldName = parts.pop();
|
||||||
const parentPath = parts.join('/');
|
const parentPath = parts.join('/');
|
||||||
const newPath = parentPath ? `${parentPath}/${renameValue}` : renameValue;
|
const newPath = parentPath ? `${parentPath}/${renameValue}` : renameValue;
|
||||||
|
|
||||||
// Check if this would overwrite an existing file or folder
|
// Check if this would overwrite an existing file or folder
|
||||||
const parts2 = newPath.split('/');
|
const parts2 = newPath.split('/');
|
||||||
const newName = parts2.pop();
|
const newName = parts2.pop();
|
||||||
let current = fileStructure;
|
let current = fileStructure;
|
||||||
|
|
||||||
// Navigate to parent folder
|
// Navigate to parent folder
|
||||||
for (let i = 0; i < parts2.length; i++) {
|
for (let i = 0; i < parts2.length; i++) {
|
||||||
current = current[parts2[i]].children;
|
current = current[parts2[i]].children;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current[newName] && renamePath !== newPath) {
|
if (current[newName] && renamePath !== newPath) {
|
||||||
alert(`An item named "${newName}" already exists at this location.`);
|
alert(`An item named "${newName}" already exists at this location.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the object data
|
// Get the object data
|
||||||
const pathParts = renamePath.split('/');
|
const pathParts = renamePath.split('/');
|
||||||
let curr = fileStructure;
|
let curr = fileStructure;
|
||||||
@@ -412,10 +412,10 @@ const EditorArea = ({
|
|||||||
curr = curr[pathParts[i]].children;
|
curr = curr[pathParts[i]].children;
|
||||||
}
|
}
|
||||||
const item = curr[pathParts[pathParts.length - 1]];
|
const item = curr[pathParts[pathParts.length - 1]];
|
||||||
|
|
||||||
// Delete from old location
|
// Delete from old location
|
||||||
delete curr[pathParts[pathParts.length - 1]];
|
delete curr[pathParts[pathParts.length - 1]];
|
||||||
|
|
||||||
// Add to new location
|
// Add to new location
|
||||||
const newParts = newPath.split('/');
|
const newParts = newPath.split('/');
|
||||||
curr = fileStructure;
|
curr = fileStructure;
|
||||||
@@ -423,7 +423,7 @@ const EditorArea = ({
|
|||||||
curr = curr[newParts[i]].children;
|
curr = curr[newParts[i]].children;
|
||||||
}
|
}
|
||||||
curr[newParts[newParts.length - 1]] = item;
|
curr[newParts[newParts.length - 1]] = item;
|
||||||
|
|
||||||
// If it's a file, update the files array
|
// If it's a file, update the files array
|
||||||
if (item.type === 'file') {
|
if (item.type === 'file') {
|
||||||
const fileIndex = files.findIndex(file => file.id === renamePath);
|
const fileIndex = files.findIndex(file => file.id === renamePath);
|
||||||
@@ -434,12 +434,12 @@ const EditorArea = ({
|
|||||||
id: newPath
|
id: newPath
|
||||||
};
|
};
|
||||||
setFiles(updatedFiles);
|
setFiles(updatedFiles);
|
||||||
|
|
||||||
// Update active tab if necessary
|
// Update active tab if necessary
|
||||||
if (activeTab === renamePath) {
|
if (activeTab === renamePath) {
|
||||||
setActiveTab(newPath);
|
setActiveTab(newPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update unsavedChanges
|
// Update unsavedChanges
|
||||||
if (unsavedChanges[renamePath]) {
|
if (unsavedChanges[renamePath]) {
|
||||||
const newUnsavedChanges = { ...unsavedChanges };
|
const newUnsavedChanges = { ...unsavedChanges };
|
||||||
@@ -449,7 +449,7 @@ const EditorArea = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setFileStructure({...fileStructure});
|
setFileStructure({...fileStructure});
|
||||||
setIsRenaming(false);
|
setIsRenaming(false);
|
||||||
};
|
};
|
||||||
@@ -500,7 +500,7 @@ Happy coding!`;
|
|||||||
}
|
}
|
||||||
|
|
||||||
const activeFile = files.find(file => file.id === activeTab);
|
const activeFile = files.find(file => file.id === activeTab);
|
||||||
|
|
||||||
// Calculate editor area style based on sidebar visibility
|
// Calculate editor area style based on sidebar visibility
|
||||||
const editorAreaStyle = {
|
const editorAreaStyle = {
|
||||||
marginLeft: sidebarVisible ? `${sidebarWidth}px` : '0px',
|
marginLeft: sidebarVisible ? `${sidebarWidth}px` : '0px',
|
||||||
@@ -510,23 +510,25 @@ Happy coding!`;
|
|||||||
// Modify the handleRunCode function to prompt for input first
|
// Modify the handleRunCode function to prompt for input first
|
||||||
const handleRunCode = async () => {
|
const handleRunCode = async () => {
|
||||||
if (!activeFile) return;
|
if (!activeFile) return;
|
||||||
|
|
||||||
// Show the panel
|
// Show the panel
|
||||||
setShowPanel(true);
|
setShowPanel(true);
|
||||||
if (setPanelVisible) {
|
if (setPanelVisible) {
|
||||||
setPanelVisible(true);
|
setPanelVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set state to waiting for input
|
// Set state to waiting for input
|
||||||
setWaitingForInput(true);
|
setWaitingForInput(true);
|
||||||
setActiveRunningFile(activeFile.id);
|
setActiveRunningFile(activeFile.id);
|
||||||
|
|
||||||
// Clear previous output and add new command
|
// Clear previous output and add new command
|
||||||
const fileExtension = activeFile.id.split('.').pop().toLowerCase();
|
const fileExtension = activeFile.id.split('.').pop().toLowerCase();
|
||||||
const language = getLanguageFromExtension(fileExtension);
|
const language = getLanguageFromExtension(fileExtension);
|
||||||
|
|
||||||
const newOutput = [
|
const newOutput = [
|
||||||
{ type: 'command', content: `$ run ${activeFile.id}` },
|
{ type: 'command', content: `$ run ${activeFile.id}` },
|
||||||
|
{ type: 'output', content: '------- PROGRAM EXECUTION -------' },
|
||||||
|
{ type: 'output', content: `Language: ${language}` },
|
||||||
{ type: 'output', content: 'Waiting for input (press Enter if no input is needed)...' }
|
{ type: 'output', content: 'Waiting for input (press Enter if no input is needed)...' }
|
||||||
];
|
];
|
||||||
setTerminalOutput(newOutput);
|
setTerminalOutput(newOutput);
|
||||||
@@ -535,16 +537,23 @@ Happy coding!`;
|
|||||||
// Add a new function to handle input submission
|
// Add a new function to handle input submission
|
||||||
const handleInputSubmit = async () => {
|
const handleInputSubmit = async () => {
|
||||||
if (!activeFile || !waitingForInput) return;
|
if (!activeFile || !waitingForInput) return;
|
||||||
|
|
||||||
// Set running state
|
// Set running state
|
||||||
setIsRunning(true);
|
setIsRunning(true);
|
||||||
setWaitingForInput(false);
|
setWaitingForInput(false);
|
||||||
|
|
||||||
// Add message that we're running with the input
|
// Add message that we're running with the input
|
||||||
setTerminalOutput(prev => [
|
if (userInput) {
|
||||||
...prev,
|
setTerminalOutput(prev => [
|
||||||
{ type: 'output', content: userInput ? `Using input: "${userInput}"` : 'Running without input...' }
|
...prev,
|
||||||
]);
|
{ type: 'input', content: userInput }
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
setTerminalOutput(prev => [
|
||||||
|
...prev,
|
||||||
|
{ type: 'output', content: 'Running without input...' }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
// Use API URL from environment variable
|
// Use API URL from environment variable
|
||||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
|
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
|
||||||
@@ -562,36 +571,36 @@ Happy coding!`;
|
|||||||
input: userInput
|
input: userInput
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!submitResponse.ok) {
|
if (!submitResponse.ok) {
|
||||||
throw new Error(`Server error: ${submitResponse.status}`);
|
throw new Error(`Server error: ${submitResponse.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id } = await submitResponse.json();
|
const { id } = await submitResponse.json();
|
||||||
setTerminalOutput(prev => [...prev, { type: 'output', content: `Job submitted with ID: ${id}` }]);
|
setTerminalOutput(prev => [...prev, { type: 'output', content: `Job submitted with ID: ${id}` }]);
|
||||||
|
|
||||||
// Step 2: Poll for status until completed or failed
|
// Step 2: Poll for status until completed or failed
|
||||||
let status = 'pending';
|
let status = 'pending';
|
||||||
while (status !== 'completed' && status !== 'failed') {
|
while (status !== 'completed' && status !== 'failed') {
|
||||||
// Add a small delay between polls
|
// Add a small delay between polls
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
const statusResponse = await fetch(`${apiUrl}/status?id=${id}`);
|
const statusResponse = await fetch(`${apiUrl}/status?id=${id}`);
|
||||||
if (!statusResponse.ok) {
|
if (!statusResponse.ok) {
|
||||||
throw new Error(`Status check failed: ${statusResponse.status}`);
|
throw new Error(`Status check failed: ${statusResponse.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusData = await statusResponse.json();
|
const statusData = await statusResponse.json();
|
||||||
status = statusData.status;
|
status = statusData.status;
|
||||||
|
|
||||||
// Update terminal with status (for any status type)
|
// Update terminal with status (for any status type)
|
||||||
setTerminalOutput(prev => {
|
setTerminalOutput(prev => {
|
||||||
// Update the last status message or add a new one
|
// Update the last status message or add a new one
|
||||||
const hasStatus = prev.some(line => line.content.includes('Status:'));
|
const hasStatus = prev.some(line => line.content.includes('Status:'));
|
||||||
if (hasStatus) {
|
if (hasStatus) {
|
||||||
return prev.map(line =>
|
return prev.map(line =>
|
||||||
line.content.includes('Status:')
|
line.content.includes('Status:')
|
||||||
? { ...line, content: `Status: ${status}` }
|
? { ...line, content: `Status: ${status}` }
|
||||||
: line
|
: line
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -599,36 +608,51 @@ Happy coding!`;
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the result for both completed and failed status
|
// Get the result for both completed and failed status
|
||||||
const resultResponse = await fetch(`${apiUrl}/result?id=${id}`);
|
const resultResponse = await fetch(`${apiUrl}/result?id=${id}`);
|
||||||
if (!resultResponse.ok) {
|
if (!resultResponse.ok) {
|
||||||
throw new Error(`Result fetch failed: ${resultResponse.status}`);
|
throw new Error(`Result fetch failed: ${resultResponse.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { output } = await resultResponse.json();
|
const { output } = await resultResponse.json();
|
||||||
|
|
||||||
// Format and display output
|
// Format and display output
|
||||||
const outputLines = output.split('\n').map(line => ({
|
const outputLines = [];
|
||||||
type: status === 'failed' ? 'warning' : 'output',
|
|
||||||
content: line
|
// Add a header
|
||||||
}));
|
outputLines.push({
|
||||||
|
type: status === 'failed' ? 'warning' : 'output',
|
||||||
|
content: status === 'failed'
|
||||||
|
? '------- EXECUTION FAILED -------'
|
||||||
|
: '------- EXECUTION RESULT -------'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process the output line by line
|
||||||
|
output.split('\n').forEach(line => {
|
||||||
|
// Check if this is an input line
|
||||||
|
if (line.startsWith('[Input] ')) {
|
||||||
|
outputLines.push({
|
||||||
|
type: 'input',
|
||||||
|
content: line.substring(8) // Remove the '[Input] ' prefix
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
outputLines.push({
|
||||||
|
type: status === 'failed' ? 'warning' : 'output',
|
||||||
|
content: line
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
setTerminalOutput(prev => [
|
setTerminalOutput(prev => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
|
||||||
type: status === 'failed' ? 'warning' : 'output',
|
|
||||||
content: status === 'failed'
|
|
||||||
? '------- EXECUTION FAILED -------'
|
|
||||||
: '------- EXECUTION RESULT -------'
|
|
||||||
},
|
|
||||||
...outputLines
|
...outputLines
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (status === 'failed') {
|
if (status === 'failed') {
|
||||||
console.error('Code execution failed:', output);
|
console.error('Code execution failed:', output);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setTerminalOutput(prev => [...prev, { type: 'warning', content: `Error: ${error.message}` }]);
|
setTerminalOutput(prev => [...prev, { type: 'warning', content: `Error: ${error.message}` }]);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -636,7 +660,7 @@ Happy coding!`;
|
|||||||
setIsRunning(false);
|
setIsRunning(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to convert file extension to language identifier for API
|
// Helper function to convert file extension to language identifier for API
|
||||||
const getLanguageFromExtension = (extension) => {
|
const getLanguageFromExtension = (extension) => {
|
||||||
const languageMap = {
|
const languageMap = {
|
||||||
@@ -649,7 +673,7 @@ Happy coding!`;
|
|||||||
'ts': 'typescript',
|
'ts': 'typescript',
|
||||||
'tsx': 'typescript'
|
'tsx': 'typescript'
|
||||||
};
|
};
|
||||||
|
|
||||||
return languageMap[extension] || extension;
|
return languageMap[extension] || extension;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -665,30 +689,30 @@ Happy coding!`;
|
|||||||
// Add this function above the return statement
|
// Add this function above the return statement
|
||||||
const handleDownloadFile = () => {
|
const handleDownloadFile = () => {
|
||||||
if (!activeFile) return;
|
if (!activeFile) return;
|
||||||
|
|
||||||
// Create a blob with the file content
|
// Create a blob with the file content
|
||||||
const blob = new Blob([activeFile.content], { type: 'text/plain' });
|
const blob = new Blob([activeFile.content], { type: 'text/plain' });
|
||||||
|
|
||||||
// Create a URL for the blob
|
// Create a URL for the blob
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
// Create a temporary anchor element
|
// Create a temporary anchor element
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
|
|
||||||
// Get just the filename without path
|
// Get just the filename without path
|
||||||
const fileName = activeFile.id.includes('/') ?
|
const fileName = activeFile.id.includes('/') ?
|
||||||
activeFile.id.split('/').pop() :
|
activeFile.id.split('/').pop() :
|
||||||
activeFile.id;
|
activeFile.id;
|
||||||
|
|
||||||
// Set the download attribute with the filename
|
// Set the download attribute with the filename
|
||||||
a.download = fileName;
|
a.download = fileName;
|
||||||
|
|
||||||
// Append to the document, click it, and then remove it
|
// Append to the document, click it, and then remove it
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
|
|
||||||
// Release the object URL
|
// Release the object URL
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
@@ -716,16 +740,16 @@ Happy coding!`;
|
|||||||
createNewFolder={createNewFolder}
|
createNewFolder={createNewFolder}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="editor-area" style={editorAreaStyle}>
|
<div className="editor-area" style={editorAreaStyle}>
|
||||||
<div className="editor-header">
|
<div className="editor-header">
|
||||||
<div className="editor-tabs">
|
<div className="editor-tabs">
|
||||||
{files.map((file) => {
|
{files.map((file) => {
|
||||||
// Extract just the filename without path for display
|
// Extract just the filename without path for display
|
||||||
const displayName = file.id.includes('/') ?
|
const displayName = file.id.includes('/') ?
|
||||||
file.id.split('/').pop() :
|
file.id.split('/').pop() :
|
||||||
file.id;
|
file.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={file.id}
|
key={file.id}
|
||||||
@@ -738,8 +762,8 @@ Happy coding!`;
|
|||||||
{displayName} {/* Show just filename, not full path */}
|
{displayName} {/* Show just filename, not full path */}
|
||||||
{unsavedChanges[file.id] && ' •'}
|
{unsavedChanges[file.id] && ' •'}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
className="tab-close"
|
className="tab-close"
|
||||||
onClick={(e) => handleCloseTab(e, file.id)}
|
onClick={(e) => handleCloseTab(e, file.id)}
|
||||||
>
|
>
|
||||||
<X size={12} />
|
<X size={12} />
|
||||||
@@ -747,7 +771,7 @@ Happy coding!`;
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<button
|
<button
|
||||||
className="editor-tab-new"
|
className="editor-tab-new"
|
||||||
onClick={() => setIsNewFileModalOpen(true)}
|
onClick={() => setIsNewFileModalOpen(true)}
|
||||||
title="Create new file"
|
title="Create new file"
|
||||||
@@ -755,21 +779,21 @@ Happy coding!`;
|
|||||||
<Plus size={14} />
|
<Plus size={14} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Run controls */}
|
{/* Run controls */}
|
||||||
<div className="editor-run-controls">
|
<div className="editor-run-controls">
|
||||||
{activeFile && (
|
{activeFile && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className="run-button"
|
className="run-button"
|
||||||
onClick={handleRunCode}
|
onClick={handleRunCode}
|
||||||
disabled={isRunning}
|
disabled={isRunning}
|
||||||
title="Run code"
|
title="Run code"
|
||||||
>
|
>
|
||||||
{isRunning ? <Loader size={16} className="animate-spin" /> : <Play size={16} />}
|
{isRunning ? <Loader size={16} className="animate-spin" /> : <Play size={16} />}
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="terminal-toggle-button"
|
className="terminal-toggle-button"
|
||||||
onClick={togglePanel} // Use the new function
|
onClick={togglePanel} // Use the new function
|
||||||
title="Toggle terminal"
|
title="Toggle terminal"
|
||||||
@@ -781,8 +805,8 @@ Happy coding!`;
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="monaco-container" style={{
|
<div className="monaco-container" style={{
|
||||||
height: showPanel ? `calc(100% - ${panelHeight}px - 30px)` : "100%"
|
height: showPanel ? `calc(100% - ${panelHeight}px - 30px)` : "100%"
|
||||||
}}>
|
}}>
|
||||||
{activeFile ? (
|
{activeFile ? (
|
||||||
<Editor
|
<Editor
|
||||||
@@ -834,7 +858,7 @@ Happy coding!`;
|
|||||||
document.addEventListener("mouseup", onMouseUp);
|
document.addEventListener("mouseup", onMouseUp);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Panel
|
<Panel
|
||||||
height={panelHeight}
|
height={panelHeight}
|
||||||
terminalOutput={terminalOutput}
|
terminalOutput={terminalOutput}
|
||||||
isRunning={isRunning}
|
isRunning={isRunning}
|
||||||
@@ -851,7 +875,7 @@ Happy coding!`;
|
|||||||
|
|
||||||
{/* Modify the editor-actions div to include the download button */}
|
{/* Modify the editor-actions div to include the download button */}
|
||||||
<div className="editor-actions">
|
<div className="editor-actions">
|
||||||
<button
|
<button
|
||||||
className="editor-action-button"
|
className="editor-action-button"
|
||||||
onClick={handleSaveFile}
|
onClick={handleSaveFile}
|
||||||
disabled={!activeTab || !unsavedChanges[activeTab]}
|
disabled={!activeTab || !unsavedChanges[activeTab]}
|
||||||
@@ -859,9 +883,9 @@ Happy coding!`;
|
|||||||
>
|
>
|
||||||
<Save size={16} />
|
<Save size={16} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Add download button */}
|
{/* Add download button */}
|
||||||
<button
|
<button
|
||||||
className="editor-action-button"
|
className="editor-action-button"
|
||||||
onClick={handleDownloadFile}
|
onClick={handleDownloadFile}
|
||||||
disabled={!activeTab}
|
disabled={!activeTab}
|
||||||
@@ -905,10 +929,10 @@ Happy coding!`;
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Context Menu */}
|
{/* Context Menu */}
|
||||||
{showContextMenu && (
|
{showContextMenu && (
|
||||||
<div
|
<div
|
||||||
className="context-menu"
|
className="context-menu"
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
@@ -951,27 +975,27 @@ Happy coding!`;
|
|||||||
if (file) {
|
if (file) {
|
||||||
// Create a blob with the file content
|
// Create a blob with the file content
|
||||||
const blob = new Blob([file.content], { type: 'text/plain' });
|
const blob = new Blob([file.content], { type: 'text/plain' });
|
||||||
|
|
||||||
// Create a URL for the blob
|
// Create a URL for the blob
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
// Create a temporary anchor element
|
// Create a temporary anchor element
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
|
|
||||||
// Get just the filename without path
|
// Get just the filename without path
|
||||||
const fileName = file.id.includes('/') ?
|
const fileName = file.id.includes('/') ?
|
||||||
file.id.split('/').pop() :
|
file.id.split('/').pop() :
|
||||||
file.id;
|
file.id;
|
||||||
|
|
||||||
// Set the download attribute with the filename
|
// Set the download attribute with the filename
|
||||||
a.download = fileName;
|
a.download = fileName;
|
||||||
|
|
||||||
// Append to the document, click it, and then remove it
|
// Append to the document, click it, and then remove it
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
|
|
||||||
// Release the object URL
|
// Release the object URL
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
@@ -1007,9 +1031,9 @@ Happy coding!`;
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showContextMenu && (
|
{showContextMenu && (
|
||||||
<div
|
<div
|
||||||
className="context-menu-overlay"
|
className="context-menu-overlay"
|
||||||
onClick={closeContextMenu}
|
onClick={closeContextMenu}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
const Panel = ({
|
const Panel = ({
|
||||||
height,
|
height,
|
||||||
terminalOutput = [],
|
terminalOutput = [],
|
||||||
isRunning = false,
|
isRunning = false,
|
||||||
@@ -28,26 +28,35 @@ const Panel = ({
|
|||||||
// Render output from EditorArea when available
|
// Render output from EditorArea when available
|
||||||
<>
|
<>
|
||||||
{terminalOutput.map((line, index) => (
|
{terminalOutput.map((line, index) => (
|
||||||
<div key={index} className={`terminal-line ${line.type === 'warning' ? 'terminal-warning' : 'terminal-output'}`}>
|
<div key={index} className={`terminal-line ${line.type === 'warning' ? 'terminal-warning' : line.type === 'input' ? 'terminal-input-line' : 'terminal-output'}`}>
|
||||||
{line.type === 'command' ? <span className="terminal-prompt">$</span> : ''} {line.content}
|
{line.type === 'command' ? <span className="terminal-prompt">$</span> : ''}
|
||||||
|
{line.type === 'input' ? <span className="terminal-input-marker">[Input]</span> : ''}
|
||||||
|
{line.content}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{waitingForInput && (
|
{waitingForInput && (
|
||||||
<div className="terminal-line">
|
<div className="terminal-line terminal-input-container">
|
||||||
<span className="terminal-prompt">Input:</span>
|
<div className="terminal-input-header">
|
||||||
<input
|
<span className="terminal-input-marker">Input Required:</span>
|
||||||
type="text"
|
</div>
|
||||||
className="terminal-input"
|
<div className="terminal-input-wrapper">
|
||||||
value={userInput}
|
<input
|
||||||
onChange={(e) => onUserInputChange && onUserInputChange(e.target.value)}
|
type="text"
|
||||||
placeholder="Enter input for your program here..."
|
className="terminal-input"
|
||||||
onKeyDown={(e) => {
|
value={userInput}
|
||||||
if (e.key === 'Enter' && onInputSubmit) {
|
onChange={(e) => onUserInputChange && onUserInputChange(e.target.value)}
|
||||||
onInputSubmit();
|
placeholder="Enter input for your program here..."
|
||||||
}
|
onKeyDown={(e) => {
|
||||||
}}
|
if (e.key === 'Enter' && onInputSubmit) {
|
||||||
autoFocus
|
onInputSubmit();
|
||||||
/>
|
}
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="terminal-input-help">
|
||||||
|
Press Enter to submit input
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -435,6 +435,19 @@ body {
|
|||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.terminal-input-marker {
|
||||||
|
color: #4ec9b0;
|
||||||
|
margin-right: 8px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-input-line {
|
||||||
|
color: #4ec9b0;
|
||||||
|
background-color: rgba(78, 201, 176, 0.1);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.terminal-output {
|
.terminal-output {
|
||||||
color: #888888;
|
color: #888888;
|
||||||
color: #cccccc;
|
color: #cccccc;
|
||||||
@@ -925,18 +938,43 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.terminal-input {
|
.terminal-input {
|
||||||
background-color: transparent;
|
background-color: rgba(78, 201, 176, 0.1);
|
||||||
border: none;
|
border: 1px solid rgba(78, 201, 176, 0.3);
|
||||||
color: inherit;
|
border-radius: 3px;
|
||||||
|
color: #4ec9b0;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
outline: none;
|
outline: none;
|
||||||
width: calc(100% - 60px);
|
width: calc(100% - 60px);
|
||||||
|
padding: 4px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-input:focus {
|
.terminal-input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
border-color: rgba(78, 201, 176, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-input-container {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: rgba(78, 201, 176, 0.05);
|
||||||
|
border-radius: 5px;
|
||||||
|
border-left: 3px solid #4ec9b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-input-header {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-input-wrapper {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-input-help {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888888;
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-line.info {
|
.terminal-line.info {
|
||||||
|
|||||||
137
Readme.md
137
Readme.md
@@ -1,22 +1,125 @@
|
|||||||
# Monaco Code Execution Engine
|
# Monaco Online Code Compiler
|
||||||
Monaco is a secure, containerized code execution engine that allows you to run code in multiple programming languages through a simple REST API.
|
|
||||||
|
A full-featured online code compiler with a VS Code-like interface. This project allows users to write, edit, and execute code in multiple programming languages directly in the browser.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Multi-language support: Run code in Python, Java, C, and C++
|
|
||||||
- Secure execution: All code runs in isolated Docker containers
|
|
||||||
- Resource limits: Memory, CPU, and file descriptor limits to prevent abuse
|
|
||||||
- Concurrent processing: Efficient job queue for handling multiple requests
|
|
||||||
- Simple REST API: Easy to integrate with any frontend
|
|
||||||
|
|
||||||
## Architecture
|
- **VS Code-like Interface**: Familiar editor experience with syntax highlighting, tabs, and file explorer
|
||||||
Monaco consists of several components:
|
- **Multi-language Support**: Run code in Python, JavaScript, Go, Java, C, and C++
|
||||||
|
- **Input/Output Handling**: Enter input for your programs and see the output in real-time
|
||||||
|
- **Secure Execution**: Code runs in isolated Docker containers on the backend
|
||||||
|
- **File Management**: Create, edit, and organize files and folders
|
||||||
|
|
||||||
- HTTP Handlers (handler/handler.go): Processes API requests
|
## Project Structure
|
||||||
- Execution Service (service/execution.go): Manages code execution in containers
|
|
||||||
- Job Queue (queue/queue.go): Handles concurrent execution of code submissions
|
|
||||||
- Submission Model (model/submission.go): Defines the data structure for code submissions
|
|
||||||
|
|
||||||
## Requirements
|
- **Frontend**: React-based UI with Monaco Editor
|
||||||
- Go 1.22.3 or higher
|
- **Backend**: Go-based code execution service with Docker integration
|
||||||
- Docker
|
- HTTP Handlers (internal/api/handlers): Processes API requests
|
||||||
- Network connectivity for container image pulling
|
- Execution Service (internal/executor): Manages code execution in containers
|
||||||
|
- Job Queue (internal/queue): Handles concurrent execution of code submissions
|
||||||
|
- Submission Model (internal/models): Defines the data structure for code submissions
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js 18+ for the frontend
|
||||||
|
- Go 1.22+ for the backend
|
||||||
|
- Docker for code execution
|
||||||
|
|
||||||
|
### Running the Frontend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Frontend
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The frontend will be available at http://localhost:5173
|
||||||
|
|
||||||
|
### Running the Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
go build -o monaco ./cmd/server
|
||||||
|
./monaco
|
||||||
|
```
|
||||||
|
|
||||||
|
The backend API will be available at http://localhost:8080
|
||||||
|
|
||||||
|
## Using the Online Compiler
|
||||||
|
|
||||||
|
1. **Create a File**: Click the "+" button in the editor tabs or use the file explorer
|
||||||
|
2. **Write Code**: Use the Monaco editor to write your code
|
||||||
|
3. **Run Code**: Click the "Play" button in the top right corner
|
||||||
|
4. **Enter Input**: If your program requires input, enter it in the terminal panel
|
||||||
|
5. **View Output**: See the execution results in the terminal panel
|
||||||
|
|
||||||
|
## Supported Languages
|
||||||
|
|
||||||
|
- **Python** (.py)
|
||||||
|
- **JavaScript** (.js)
|
||||||
|
- **Go** (.go)
|
||||||
|
- **Java** (.java)
|
||||||
|
- **C** (.c)
|
||||||
|
- **C++** (.cpp)
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
name = input("Enter your name: ")
|
||||||
|
print(f"Hello, {name}!")
|
||||||
|
for i in range(5):
|
||||||
|
print(f"Count: {i}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const readline = require('readline');
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout
|
||||||
|
});
|
||||||
|
|
||||||
|
rl.question('Enter your name: ', (name) => {
|
||||||
|
console.log(`Hello, ${name}!`);
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
console.log(`Count: ${i}`);
|
||||||
|
}
|
||||||
|
rl.close();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Print("Enter your name: ")
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
name, _ := reader.ReadString('\n')
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
fmt.Printf("Hello, %s!\n", name)
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
fmt.Printf("Count: %d\n", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- All code is executed in isolated Docker containers
|
||||||
|
- Network access is disabled
|
||||||
|
- Memory and CPU limits are enforced
|
||||||
|
- Execution timeouts prevent infinite loops
|
||||||
@@ -2,7 +2,6 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -144,6 +143,7 @@ func (h *Handler) ResultHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
"status": submission.Status,
|
"status": submission.Status,
|
||||||
"language": submission.Language,
|
"language": submission.Language,
|
||||||
"output": submission.Output,
|
"output": submission.Output,
|
||||||
|
"input": submission.Input,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add error information if available
|
// Add error information if available
|
||||||
@@ -190,6 +190,55 @@ func (h *Handler) QueueStatsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
json.NewEncoder(w).Encode(response)
|
json.NewEncoder(w).Encode(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SubmitInputHandler handles interactive input submission
|
||||||
|
func (h *Handler) SubmitInputHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Only allow POST method
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the request body
|
||||||
|
var inputRequest struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Input string `json:"input"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&inputRequest); err != nil {
|
||||||
|
http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the request
|
||||||
|
if inputRequest.ID == "" {
|
||||||
|
http.Error(w, "ID is required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the submission from the map
|
||||||
|
h.mu.Lock()
|
||||||
|
submission, exists := h.submissions[inputRequest.ID]
|
||||||
|
h.mu.Unlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
http.Error(w, "Submission not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the submission is waiting for input
|
||||||
|
if submission.Status != "waiting_for_input" {
|
||||||
|
http.Error(w, "Submission is not waiting for input", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the input to the execution service
|
||||||
|
h.executionService.SubmitInput(submission, inputRequest.Input)
|
||||||
|
|
||||||
|
// Return success response
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"status": "input_submitted"})
|
||||||
|
}
|
||||||
|
|
||||||
// HealthCheckHandler handles health check requests
|
// HealthCheckHandler handles health check requests
|
||||||
func (h *Handler) HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// Only allow GET method
|
// Only allow GET method
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ func SetupRoutes() http.Handler {
|
|||||||
mux.HandleFunc("/submit", h.SubmitHandler)
|
mux.HandleFunc("/submit", h.SubmitHandler)
|
||||||
mux.HandleFunc("/status", h.StatusHandler)
|
mux.HandleFunc("/status", h.StatusHandler)
|
||||||
mux.HandleFunc("/result", h.ResultHandler)
|
mux.HandleFunc("/result", h.ResultHandler)
|
||||||
|
mux.HandleFunc("/submit-input", h.SubmitInputHandler)
|
||||||
mux.HandleFunc("/queue-stats", h.QueueStatsHandler)
|
mux.HandleFunc("/queue-stats", h.QueueStatsHandler)
|
||||||
mux.HandleFunc("/health", h.HealthCheckHandler)
|
mux.HandleFunc("/health", h.HealthCheckHandler)
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/arnab-afk/monaco/internal/models"
|
"github.com/arnab-afk/monaco/internal/models"
|
||||||
@@ -19,6 +20,9 @@ import (
|
|||||||
// ExecutionService manages code execution
|
// ExecutionService manages code execution
|
||||||
type ExecutionService struct {
|
type ExecutionService struct {
|
||||||
queue *queue.JobQueue
|
queue *queue.JobQueue
|
||||||
|
mu sync.Mutex
|
||||||
|
// Map of submission ID to input channel for interactive programs
|
||||||
|
inputChannels map[string]chan string
|
||||||
}
|
}
|
||||||
|
|
||||||
// CodeExecutionJob represents a code execution job
|
// CodeExecutionJob represents a code execution job
|
||||||
@@ -30,7 +34,8 @@ type CodeExecutionJob struct {
|
|||||||
// NewExecutionService creates a new execution service
|
// NewExecutionService creates a new execution service
|
||||||
func NewExecutionService() *ExecutionService {
|
func NewExecutionService() *ExecutionService {
|
||||||
return &ExecutionService{
|
return &ExecutionService{
|
||||||
queue: queue.NewJobQueue(5), // 5 concurrent workers
|
queue: queue.NewJobQueue(5), // 5 concurrent workers
|
||||||
|
inputChannels: make(map[string]chan string),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -601,6 +606,25 @@ func (s *ExecutionService) updateSubmissionResult(submission *models.CodeSubmiss
|
|||||||
submission.Output = formattedOutput + rawOutput
|
submission.Output = formattedOutput + rawOutput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SubmitInput submits input to a running interactive program
|
||||||
|
func (s *ExecutionService) SubmitInput(submission *models.CodeSubmission, input string) {
|
||||||
|
s.mu.Lock()
|
||||||
|
inputChan, exists := s.inputChannels[submission.ID]
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
log.Printf("[ERROR] No input channel found for submission %s", submission.ID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the input to the channel
|
||||||
|
inputChan <- input
|
||||||
|
|
||||||
|
// Update the submission status
|
||||||
|
submission.Status = "running"
|
||||||
|
submission.Output += "[Input] " + input + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
// GetQueueStats returns statistics about the job queue
|
// GetQueueStats returns statistics about the job queue
|
||||||
func (s *ExecutionService) GetQueueStats() models.QueueStats {
|
func (s *ExecutionService) GetQueueStats() models.QueueStats {
|
||||||
return s.queue.GetStats()
|
return s.queue.GetStats()
|
||||||
|
|||||||
@@ -4,16 +4,18 @@ import "time"
|
|||||||
|
|
||||||
// CodeSubmission represents a code submission for execution
|
// CodeSubmission represents a code submission for execution
|
||||||
type CodeSubmission struct {
|
type CodeSubmission struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
Language string `json:"language"`
|
Language string `json:"language"`
|
||||||
Input string `json:"input"`
|
Input string `json:"input"`
|
||||||
Status string `json:"status"` // "pending", "queued", "running", "completed", "failed"
|
Status string `json:"status"` // "pending", "queued", "running", "waiting_for_input", "completed", "failed"
|
||||||
QueuedAt time.Time `json:"queuedAt,omitempty"`
|
QueuedAt time.Time `json:"queuedAt,omitempty"`
|
||||||
StartedAt time.Time `json:"startedAt,omitempty"`
|
StartedAt time.Time `json:"startedAt,omitempty"`
|
||||||
CompletedAt time.Time `json:"completedAt,omitempty"`
|
CompletedAt time.Time `json:"completedAt,omitempty"`
|
||||||
Output string `json:"output,omitempty"`
|
Output string `json:"output,omitempty"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
|
IsInteractive bool `json:"isInteractive,omitempty"` // Whether the program requires interactive input
|
||||||
|
CurrentPrompt string `json:"currentPrompt,omitempty"` // Current input prompt if waiting for input
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecutionResult represents the result of code execution
|
// ExecutionResult represents the result of code execution
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user