chowdary1's picture
V2 Full Sync: 4-mode architecture, compiler, RAG, neon theme, round detection fix, camera toggle
7fff83a
Raw History Blame Contribute Delete
15.5 kB
/**
* CodeEditor Component - Lightweight Code Editor for Coding Rounds
*
* Features:
* - Multi-language support (Python, JavaScript, Java, C++, etc.)
* - Syntax highlighting via simple CSS classes
* - Code execution with output display
* - Keyboard shortcuts (Ctrl+Enter to run)
* - Line numbers
* - Auto-indentation
*/
import React, { useState, useRef, useEffect, useCallback } from 'react';
import {
Play, Square, RotateCcw, Copy, Check, ChevronDown,
Terminal, Code, Loader2, AlertCircle, CheckCircle2,
Maximize2, Minimize2
} from 'lucide-react';
import { getToken } from '../services/api';
import './CodeEditor.css';
// Supported Languages
const LANGUAGES = [
{ id: 'python', name: 'Python', icon: '🐍', extension: '.py' },
{ id: 'javascript', name: 'JavaScript', icon: '🟨', extension: '.js' },
{ id: 'typescript', name: 'TypeScript', icon: '🔷', extension: '.ts' },
{ id: 'java', name: 'Java', icon: '☕', extension: '.java' },
{ id: 'cpp', name: 'C++', icon: '⚡', extension: '.cpp' },
{ id: 'c', name: 'C', icon: '🔵', extension: '.c' },
{ id: 'go', name: 'Go', icon: '🔹', extension: '.go' },
{ id: 'ruby', name: 'Ruby', icon: '💎', extension: '.rb' },
{ id: 'php', name: 'PHP', icon: '🐘', extension: '.php' },
];
// Default code templates
const DEFAULT_TEMPLATES = {
python: `# Python Solution
def solution():
# Write your code here
print("Hello, World!")
solution()`,
javascript: `// JavaScript Solution
function solution() {
// Write your code here
console.log("Hello, World!");
}
solution();`,
typescript: `// TypeScript Solution
function solution(): void {
// Write your code here
console.log("Hello, World!");
}
solution();`,
java: `// Java Solution
public class Solution {
public static void main(String[] args) {
// Write your code here
System.out.println("Hello, World!");
}
}`,
cpp: `// C++ Solution
#include <iostream>
using namespace std;
int main() {
// Write your code here
cout << "Hello, World!" << endl;
return 0;
}`,
c: `// C Solution
#include <stdio.h>
int main() {
// Write your code here
printf("Hello, World!\\n");
return 0;
}`,
go: `// Go Solution
package main
import "fmt"
func main() {
// Write your code here
fmt.Println("Hello, World!")
}`,
ruby: `# Ruby Solution
def solution
# Write your code here
puts "Hello, World!"
end
solution`,
php: `<?php
// PHP Solution
function solution() {
// Write your code here
echo "Hello, World!\\n";
}
solution();
?>`,
};
const CodeEditor = ({
initialCode = '',
initialLanguage = 'python',
onCodeChange,
onRun,
readOnly = false,
className = '',
showOutput = true,
apiEndpoint = '/api/compiler/execute',
}) => {
const [code, setCode] = useState(initialCode || DEFAULT_TEMPLATES[initialLanguage] || '');
const [language, setLanguage] = useState(initialLanguage);
const [output, setOutput] = useState('');
const [isRunning, setIsRunning] = useState(false);
const [error, setError] = useState(null);
const [executionTime, setExecutionTime] = useState(null);
const [copied, setCopied] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const [showLanguageDropdown, setShowLanguageDropdown] = useState(false);
const [stdin, setStdin] = useState('');
const [showStdin, setShowStdin] = useState(false);
const textareaRef = useRef(null);
const lineNumbersRef = useRef(null);
const dropdownRef = useRef(null);
// Calculate line numbers
const lineCount = code.split('\n').length;
const lineNumbers = Array.from({ length: lineCount }, (_, i) => i + 1);
// Sync scroll between textarea and line numbers
const handleScroll = useCallback((e) => {
if (lineNumbersRef.current) {
lineNumbersRef.current.scrollTop = e.target.scrollTop;
}
}, []);
// Handle code change
const handleCodeChange = (e) => {
const newCode = e.target.value;
setCode(newCode);
onCodeChange?.(newCode, language);
};
// Handle language change
const handleLanguageChange = (langId) => {
setLanguage(langId);
// If code is empty or default template, switch to new template
if (!code || Object.values(DEFAULT_TEMPLATES).includes(code)) {
setCode(DEFAULT_TEMPLATES[langId] || '');
}
setShowLanguageDropdown(false);
onCodeChange?.(code, langId);
};
// Handle Tab key for indentation
const handleKeyDown = (e) => {
if (e.key === 'Tab') {
e.preventDefault();
const start = e.target.selectionStart;
const end = e.target.selectionEnd;
const newCode = code.substring(0, start) + ' ' + code.substring(end);
setCode(newCode);
// Move cursor after the inserted spaces
setTimeout(() => {
e.target.selectionStart = e.target.selectionEnd = start + 4;
}, 0);
}
// Ctrl/Cmd + Enter to run
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
handleRun();
}
};
// Run code
const handleRun = async () => {
if (isRunning || !code.trim()) return;
setIsRunning(true);
setError(null);
setOutput('');
setExecutionTime(null);
const startTime = Date.now();
try {
const token = getToken();
const response = await fetch(apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
},
body: JSON.stringify({
code,
language,
stdin: stdin || undefined,
timeout: 10
}),
});
const data = await response.json();
const elapsed = Date.now() - startTime;
setExecutionTime(elapsed);
// Handle HTTP error responses (e.g., 503 when runtime not installed)
if (!response.ok) {
const errorMsg = data.detail || data.error || `Server error (${response.status})`;
setError(errorMsg);
onRun?.({ success: false, error: errorMsg, language, executionTime: elapsed });
} else if (data.success) {
setOutput(data.output || 'Program executed successfully (no output)');
onRun?.({ success: true, output: data.output, language, executionTime: elapsed });
} else {
setError(data.error || data.detail || 'Execution failed');
setOutput(data.output || '');
onRun?.({ success: false, error: data.error, language, executionTime: elapsed });
}
} catch (err) {
const elapsed = Date.now() - startTime;
setExecutionTime(elapsed);
setError(`Network error: ${err.message}`);
onRun?.({ success: false, error: err.message, language, executionTime: elapsed });
} finally {
setIsRunning(false);
}
};
// Reset code to template
const handleReset = () => {
setCode(DEFAULT_TEMPLATES[language] || '');
setOutput('');
setError(null);
setExecutionTime(null);
};
// Copy code
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
};
// Close dropdown on outside click
useEffect(() => {
const handleClickOutside = (e) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
setShowLanguageDropdown(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const currentLang = LANGUAGES.find(l => l.id === language) || LANGUAGES[0];
return (
<div className={`code-editor ${isExpanded ? 'expanded' : ''} ${className}`}>
{/* Toolbar */}
<div className="code-editor-toolbar">
<div className="toolbar-left">
{/* Language Selector */}
<div className="language-selector" ref={dropdownRef}>
<button
className="language-btn"
onClick={() => setShowLanguageDropdown(!showLanguageDropdown)}
disabled={readOnly}
>
<span className="lang-icon">{currentLang.icon}</span>
<span className="lang-name">{currentLang.name}</span>
<ChevronDown size={14} className={showLanguageDropdown ? 'rotated' : ''} />
</button>
{showLanguageDropdown && (
<div className="language-dropdown">
{LANGUAGES.map((lang) => (
<button
key={lang.id}
className={`lang-option ${lang.id === language ? 'active' : ''}`}
onClick={() => handleLanguageChange(lang.id)}
>
<span className="lang-icon">{lang.icon}</span>
<span className="lang-name">{lang.name}</span>
{lang.id === language && <CheckCircle2 size={14} />}
</button>
))}
</div>
)}
</div>
</div>
<div className="toolbar-right">
{/* Input Toggle */}
<button
className={`toolbar-btn ${showStdin ? 'active' : ''}`}
onClick={() => setShowStdin(!showStdin)}
title="Standard Input"
>
<Terminal size={14} />
</button>
{/* Copy */}
<button
className="toolbar-btn"
onClick={handleCopy}
title="Copy Code"
>
{copied ? <Check size={14} /> : <Copy size={14} />}
</button>
{/* Reset */}
<button
className="toolbar-btn"
onClick={handleReset}
title="Reset Code"
disabled={readOnly}
>
<RotateCcw size={14} />
</button>
{/* Expand */}
<button
className="toolbar-btn"
onClick={() => setIsExpanded(!isExpanded)}
title={isExpanded ? "Minimize" : "Expand"}
>
{isExpanded ? <Minimize2 size={14} /> : <Maximize2 size={14} />}
</button>
{/* Run Button */}
<button
className={`run-btn ${isRunning ? 'running' : ''}`}
onClick={handleRun}
disabled={isRunning || !code.trim() || readOnly}
>
{isRunning ? (
<>
<Loader2 size={14} className="spin" />
<span>Running...</span>
</>
) : (
<>
<Play size={14} />
<span>Run</span>
</>
)}
</button>
</div>
</div>
{/* Standard Input (Optional) */}
{showStdin && (
<div className="stdin-container">
<label>
<Terminal size={12} />
<span>Standard Input</span>
</label>
<textarea
value={stdin}
onChange={(e) => setStdin(e.target.value)}
placeholder="Enter input values here (one per line)..."
rows={3}
className="stdin-textarea"
/>
</div>
)}
{/* Editor Area */}
<div className="code-editor-body">
{/* Line Numbers */}
<div className="line-numbers" ref={lineNumbersRef}>
{lineNumbers.map((num) => (
<div key={num} className="line-number">{num}</div>
))}
</div>
{/* Code Area */}
<textarea
ref={textareaRef}
value={code}
onChange={handleCodeChange}
onKeyDown={handleKeyDown}
onScroll={handleScroll}
className={`code-textarea language-${language}`}
placeholder="Write your code here..."
spellCheck={false}
autoCapitalize="off"
autoCorrect="off"
readOnly={readOnly}
/>
</div>
{/* Output Panel */}
{showOutput && (output || error) && (
<div className={`output-panel ${error ? 'has-error' : 'success'}`}>
<div className="output-header">
<div className="output-title">
{error ? (
<>
<AlertCircle size={14} />
<span>Error</span>
</>
) : (
<>
<CheckCircle2 size={14} />
<span>Output</span>
</>
)}
</div>
{executionTime && (
<span className="execution-time">{executionTime}ms</span>
)}
</div>
<pre className="output-content">
{error && <div className="error-text">{error}</div>}
{output}
</pre>
</div>
)}
{/* Keyboard Shortcut Hint */}
<div className="keyboard-hint">
Press <kbd>Ctrl</kbd> + <kbd>Enter</kbd> to run
</div>
</div>
);
};
export default CodeEditor;