input channel mapping update

This commit is contained in:
2025-04-21 15:05:17 +05:30
parent 86dcfa2a4a
commit c143efa70e
9 changed files with 433 additions and 183 deletions

View File

@@ -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={{

View File

@@ -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>
)} )}
</> </>

View File

@@ -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
View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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.