some kinda

This commit is contained in:
5t4l1n
2025-07-26 22:20:50 +05:30
parent 818a268038
commit 8c56eb9e36
20 changed files with 5544 additions and 155 deletions
+4
View File
@@ -0,0 +1,4 @@
from pymongo import MongoClient
db = MongoClient("mongodb://localhost:27017").openlearnx
exam = db.exams.find_one({"_id": ObjectId("6884f04c6ca73cc9032deaf9")})
print(exam["exam_code"]) # this is what participants must type
+461 -56
View File
@@ -7,142 +7,547 @@ from mongo_service import MongoService
from web3_service import Web3Service
import logging
# Import all route blueprints
from routes import auth, test_flow, certificate, dashboard, courses, quizzes, admin
# Load environment variables first
load_dotenv()
# Import all route blueprints
from routes import auth, test_flow, certificate, dashboard, courses, quizzes, admin, exam, compiler
# Import services after loading env vars
try:
from services.wallet_service import wallet_service
from services.real_compiler_service import real_compiler_service
WALLET_SERVICE_AVAILABLE = True
COMPILER_SERVICE_AVAILABLE = True
except ImportError as e:
logging.warning(f"Service import failed: {e}")
wallet_service = None
real_compiler_service = None
WALLET_SERVICE_AVAILABLE = False
COMPILER_SERVICE_AVAILABLE = False
# Initialize Flask app
app = Flask(__name__)
# Enhanced CORS configuration for admin panel with credentials support
# Enhanced CORS configuration for coding exam platform
CORS(app, resources={
r"/api/*": {
"origins": ["http://localhost:3000", "http://127.0.0.1:3000"],
"origins": [
"http://localhost:3000",
"http://127.0.0.1:3000",
"http://localhost:3001",
"http://127.0.0.1:3001"
],
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
"allow_headers": ["Content-Type", "Authorization"],
"supports_credentials": True # ✅ Added for admin authentication
"allow_headers": [
"Content-Type",
"Authorization",
"X-Requested-With",
"Accept",
"Origin"
],
"supports_credentials": True,
"expose_headers": ["Authorization"]
}
})
# Configuration
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'your-secret-key')
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'openlearnx-secret-key-2024')
app.config['MONGODB_URI'] = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/')
app.config['WEB3_PROVIDER_URL'] = os.getenv('WEB3_PROVIDER_URL', 'http://127.0.0.1:8545')
app.config['CONTRACT_ADDRESS'] = os.getenv('CONTRACT_ADDRESS')
app.config['MINTER_PRIVATE_KEY'] = os.getenv('MINTER_PRIVATE_KEY')
app.config['CONTRACT_ADDRESS'] = os.getenv('CONTRACT_ADDRESS', '0x739f0aCef964f87Bc7974D972a811f8417d74B4C')
app.config['MINTER_PRIVATE_KEY'] = os.getenv('MINTER_PRIVATE_KEY', '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80')
app.config['ADMIN_TOKEN'] = os.getenv('ADMIN_TOKEN', 'admin-secret-key')
# Initialize services
# Blockchain configuration
app.config['IPFS_GATEWAY'] = os.getenv('IPFS_GATEWAY', 'https://ipfs.infura.io:5001')
app.config['IPFS_PROJECT_ID'] = os.getenv('IPFS_PROJECT_ID')
app.config['IPFS_PROJECT_SECRET'] = os.getenv('IPFS_PROJECT_SECRET')
# Configure logging BEFORE initializing services
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(),
logging.FileHandler('openlearnx.log') if os.access('.', os.W_OK) else logging.NullHandler()
]
)
logger = logging.getLogger(__name__)
# Initialize services with error handling
try:
mongo_service = MongoService(app.config['MONGODB_URI'])
web3_service = Web3Service(app.config['WEB3_PROVIDER_URL'], app.config['CONTRACT_ADDRESS'])
app.config['MONGO_SERVICE'] = mongo_service
MONGO_SERVICE_AVAILABLE = True
except Exception as e:
logging.error(f"Failed to initialize MongoDB service: {e}")
mongo_service = None
MONGO_SERVICE_AVAILABLE = False
try:
web3_service = Web3Service(
app.config['WEB3_PROVIDER_URL'],
app.config['CONTRACT_ADDRESS']
)
app.config['WEB3_SERVICE'] = web3_service
WEB3_SERVICE_AVAILABLE = True
except Exception as e:
logging.error(f"Failed to initialize Web3 service: {e}")
web3_service = None
WEB3_SERVICE_AVAILABLE = False
# Make services available to routes
app.config['MONGO_SERVICE'] = mongo_service
app.config['WEB3_SERVICE'] = web3_service
if WALLET_SERVICE_AVAILABLE:
app.config['WALLET_SERVICE'] = wallet_service
# Register all blueprints
app.register_blueprint(auth.bp, url_prefix='/api/auth')
app.register_blueprint(test_flow.bp, url_prefix='/api/test')
app.register_blueprint(certificate.bp, url_prefix='/api/certificate')
app.register_blueprint(dashboard.bp, url_prefix='/api/dashboard')
app.register_blueprint(courses.bp, url_prefix='/api/courses')
app.register_blueprint(quizzes.bp, url_prefix='/api/quizzes')
app.register_blueprint(admin.bp, url_prefix="/api/admin")
if COMPILER_SERVICE_AVAILABLE:
app.config['REAL_COMPILER_SERVICE'] = real_compiler_service
# ✅ DEFINE check_docker_availability BEFORE using it
def check_docker_availability():
"""Check if Docker is available for compiler service"""
try:
import docker
client = docker.from_env()
client.ping()
return True
except Exception:
return False
# Register all blueprints with error handling
blueprints = [
(auth.bp, '/api/auth'),
(test_flow.bp, '/api/test'),
(certificate.bp, '/api/certificate'),
(dashboard.bp, '/api/dashboard'),
(courses.bp, '/api/courses'),
(quizzes.bp, '/api/quizzes'),
(admin.bp, '/api/admin'),
(exam.bp, '/api/exam'), # Coding exam routes
(compiler.bp, '/api/compiler'), # Compiler routes
]
for blueprint, url_prefix in blueprints:
try:
app.register_blueprint(blueprint, url_prefix=url_prefix)
logging.info(f"✅ Registered blueprint: {url_prefix}")
print(f"✅ Registered blueprint: {url_prefix}")
except Exception as e:
logging.error(f"❌ Failed to register blueprint {url_prefix}: {e}")
print(f"❌ Failed to register blueprint {url_prefix}: {e}")
# Debug routes
@app.route('/debug/routes')
def debug_routes():
"""Debug route to see all registered routes"""
routes = []
for rule in app.url_map.iter_rules():
routes.append({
'endpoint': rule.endpoint,
'methods': list(rule.methods),
'rule': str(rule)
})
return jsonify({
"total_routes": len(routes),
"routes": sorted(routes, key=lambda x: x['rule'])
})
@app.route('/debug/exam-routes')
def debug_exam_routes():
"""Debug exam-specific routes"""
exam_routes = []
for rule in app.url_map.iter_rules():
if '/exam' in str(rule):
exam_routes.append({
'endpoint': rule.endpoint,
'methods': list(rule.methods),
'rule': str(rule)
})
return jsonify({
"exam_routes": exam_routes,
"exam_blueprint_registered": hasattr(exam, 'bp')
})
@app.route('/debug/services')
def debug_services():
"""Debug service availability"""
return jsonify({
"services": {
"mongodb": MONGO_SERVICE_AVAILABLE,
"web3": WEB3_SERVICE_AVAILABLE,
"wallet": WALLET_SERVICE_AVAILABLE,
"compiler": COMPILER_SERVICE_AVAILABLE,
"docker": check_docker_availability()
},
"app_config_keys": list(app.config.keys()),
"blueprint_count": len(app.blueprints)
})
# Direct exam test route
@app.route('/api/exam/test-direct', methods=['GET', 'POST', 'OPTIONS'])
def test_exam_direct():
"""Direct test route for exam functionality"""
if request.method == "OPTIONS":
response = jsonify({'status': 'ok'})
response.headers.add("Access-Control-Allow-Origin", "*")
response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization")
response.headers.add("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
return response
return jsonify({
"success": True,
"message": "Direct exam route is working",
"method": request.method,
"timestamp": os.popen('date').read().strip(),
"data": request.json if request.method == "POST" else None
})
# Health check endpoints
@app.route('/')
def health_check():
return jsonify({
"status": "OpenLearnX API is running",
"version": "1.0.0",
"version": "2.0.0",
"features": {
"blockchain": WEB3_SERVICE_AVAILABLE,
"coding_exams": COMPILER_SERVICE_AVAILABLE,
"wallet_auth": WALLET_SERVICE_AVAILABLE,
"database": MONGO_SERVICE_AVAILABLE,
"real_compiler": COMPILER_SERVICE_AVAILABLE
},
"endpoints": {
"auth": "/api/auth",
"courses": "/api/courses",
"admin": "/api/admin",
"dashboard": "/api/dashboard",
"certificates": "/api/certificate",
"quizzes": "/api/quizzes"
}
"quizzes": "/api/quizzes",
"coding_exams": "/api/exam",
"compiler": "/api/compiler"
},
"debug_endpoints": [
"/debug/routes",
"/debug/exam-routes",
"/debug/services",
"/api/exam/test-direct"
]
})
@app.route('/api/health')
def api_health():
"""Comprehensive API health check"""
health_status = {
"status": "healthy",
"timestamp": os.popen('date').read().strip(),
"services": {
"mongodb": MONGO_SERVICE_AVAILABLE,
"web3": WEB3_SERVICE_AVAILABLE,
"wallet": WALLET_SERVICE_AVAILABLE,
"compiler": COMPILER_SERVICE_AVAILABLE,
"docker": check_docker_availability()
},
"configuration": {
"cors_enabled": True,
"debug_mode": app.debug,
"secret_key_set": bool(app.config.get('SECRET_KEY')),
"admin_token_set": bool(app.config.get('ADMIN_TOKEN'))
},
"blueprints_registered": list(app.blueprints.keys())
}
# Check MongoDB connection
if MONGO_SERVICE_AVAILABLE:
try:
from pymongo import MongoClient
client = MongoClient(app.config['MONGODB_URI'])
client.admin.command('ismaster')
health_status["services"]["mongodb_connection"] = "connected"
except Exception as e:
health_status["services"]["mongodb_connection"] = f"error: {str(e)}"
health_status["status"] = "degraded"
# Check Web3 connection
if WEB3_SERVICE_AVAILABLE:
try:
if web3_service and web3_service.w3.is_connected():
health_status["services"]["web3_connection"] = "connected"
else:
health_status["services"]["web3_connection"] = "disconnected"
health_status["status"] = "degraded"
except Exception as e:
health_status["services"]["web3_connection"] = f"error: {str(e)}"
health_status["status"] = "degraded"
status_code = 200 if health_status["status"] == "healthy" else 503
return jsonify(health_status), status_code
@app.route('/api/admin/health')
def admin_health():
"""Admin-specific health check"""
return jsonify({
"status": "Admin API is running",
"admin_token_configured": bool(app.config.get('ADMIN_TOKEN')),
"admin_endpoints": [
"/api/admin/dashboard",
"/api/admin/courses",
"/api/admin/courses/<id>",
"/api/admin/test" # ✅ Added test endpoint
"/api/admin/test",
"/api/admin/initialize"
],
"exam_endpoints": [
"/api/exam/create-exam",
"/api/exam/join-exam",
"/api/exam/join-exam-wallet",
"/api/exam/start-exam",
"/api/exam/submit-solution",
"/api/exam/leaderboard/<exam_code>",
"/api/exam/host-dashboard/<exam_code>"
],
"compiler_endpoints": [
"/api/compiler/languages",
"/api/compiler/execute",
"/api/compiler/execute-async",
"/api/compiler/status/<execution_id>",
"/api/compiler/test",
"/api/compiler/stats"
]
})
# Error handlers
@app.errorhandler(404)
def not_found(error):
return jsonify({"error": "Endpoint not found"}), 404
return jsonify({
"error": "Endpoint not found",
"message": "The requested API endpoint does not exist",
"available_endpoints": [
"/api/auth", "/api/courses", "/api/admin",
"/api/exam", "/api/dashboard", "/api/certificate",
"/api/compiler"
],
"debug_endpoints": [
"/debug/routes",
"/debug/exam-routes",
"/debug/services"
]
}), 404
@app.errorhandler(500)
def internal_error(error):
app.logger.error(f"Internal server error: {str(error)}")
return jsonify({"error": "Internal server error"}), 500
return jsonify({
"error": "Internal server error",
"message": "An unexpected error occurred on the server"
}), 500
@app.errorhandler(403)
def forbidden(error):
return jsonify({
"error": "Forbidden",
"message": "Access denied - check your authentication"
}), 403
@app.errorhandler(401)
def unauthorized(error):
return jsonify({
"error": "Unauthorized",
"message": "Authentication required"
}), 401
@app.errorhandler(Exception)
def handle_error(error):
app.logger.error(f"Unhandled error: {str(error)}")
return jsonify({"error": "An unexpected error occurred"}), 500
# Enable logging for admin operations
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' # ✅ Enhanced logging format
)
logger = logging.getLogger(__name__)
return jsonify({
"error": "An unexpected error occurred",
"type": type(error).__name__
}), 500
# Request logging and CORS handling
@app.before_request
def log_request_info():
"""Enhanced request logging"""
if '/api/admin' in request.path:
# ✅ Enhanced admin request logging
auth_header = request.headers.get('Authorization', 'No auth header')
logger.info(f"Admin request: {request.method} {request.path} | Auth: {auth_header}")
logger.info(f"Admin request: {request.method} {request.path} | Auth: {auth_header[:20]}...")
if '/api/exam' in request.path:
logger.info(f"Exam request: {request.method} {request.path}")
print(f"📝 Exam request: {request.method} {request.path}")
if '/api/compiler' in request.path:
logger.info(f"Compiler request: {request.method} {request.path}")
# ✅ Add OPTIONS handler for CORS preflight
@app.before_request
def handle_preflight():
"""Handle CORS preflight requests"""
if request.method == "OPTIONS":
response = jsonify()
response = jsonify({'status': 'ok'})
response.headers.add("Access-Control-Allow-Origin", "*")
response.headers.add('Access-Control-Allow-Headers', "*")
response.headers.add('Access-Control-Allow-Methods', "*")
response.headers.add('Access-Control-Allow-Headers', "Content-Type,Authorization")
response.headers.add('Access-Control-Allow-Methods', "GET,POST,PUT,DELETE,OPTIONS")
response.headers.add('Access-Control-Allow-Credentials', 'true')
return response
if __name__ == '__main__':
@app.after_request
def after_request(response):
"""Add security headers"""
response.headers.add('X-Content-Type-Options', 'nosniff')
response.headers.add('X-Frame-Options', 'DENY')
response.headers.add('X-XSS-Protection', '1; mode=block')
return response
# Startup function
def initialize_application():
"""Initialize application with comprehensive error handling"""
try:
logger.info("🚀 Initializing OpenLearnX Backend...")
print("🚀 Initializing OpenLearnX Backend...")
# Test MongoDB connection
if MONGO_SERVICE_AVAILABLE:
try:
# ✅ Enhanced database initialization with better error handling
loop = asyncio.get_event_loop()
loop.run_until_complete(mongo_service.init_db())
logger.info("✅ Database initialized successfully")
print("✅ Database initialized successfully")
# ✅ Test MongoDB connection
from pymongo import MongoClient
client = MongoClient(app.config['MONGODB_URI'])
client.admin.command('ismaster')
logger.info("✅ MongoDB connection verified")
print("✅ MongoDB connection verified")
except Exception as e:
logger.error(f"❌ MongoDB initialization failed: {e}")
print(f"❌ MongoDB initialization failed: {e}")
logger.info("✅ OpenLearnX backend starting...")
logger.info(f"✅ Admin panel available at: http://localhost:3000/admin/login")
logger.info(f"✅ API health check: http://127.0.0.1:5000")
logger.info(f"✅ Admin health check: http://127.0.0.1:5000/api/admin/health")
# Test Web3 connection
if WEB3_SERVICE_AVAILABLE:
try:
if web3_service.w3.is_connected():
logger.info("✅ Web3 connection verified")
print("✅ Web3 connection verified")
else:
logger.warning("⚠️ Web3 connection failed - blockchain features disabled")
print("⚠️ Web3 connection failed - blockchain features disabled")
except Exception as e:
logger.warning(f"⚠️ Web3 connection error: {e}")
print(f"⚠️ Web3 connection error: {e}")
# ✅ Log admin token for debugging
admin_token = os.getenv('ADMIN_TOKEN', 'admin-secret-key')
logger.info(f"✅ Admin token configured: {admin_token[:8]}...")
# Test Docker connection for compiler
if COMPILER_SERVICE_AVAILABLE:
try:
docker_available = check_docker_availability()
if docker_available:
logger.info("✅ Docker connection verified - Real compiler available")
print("✅ Docker connection verified - Real compiler available")
else:
logger.warning("⚠️ Docker not available - Compiler features limited")
print("⚠️ Docker not available - Compiler features limited")
except Exception as e:
logger.warning(f"⚠️ Docker connection error: {e}")
print(f"⚠️ Docker connection error: {e}")
# Log configuration
logger.info("📋 Configuration Summary:")
print("📋 Configuration Summary:")
config_items = [
("MongoDB", MONGO_SERVICE_AVAILABLE),
("Blockchain", WEB3_SERVICE_AVAILABLE),
("Wallet Service", WALLET_SERVICE_AVAILABLE),
("Compiler Service", COMPILER_SERVICE_AVAILABLE),
("Docker", check_docker_availability())
]
for name, available in config_items:
status = "✅ Connected" if available else "❌ Unavailable"
logger.info(f"{name}: {status}")
print(f"{name}: {status}")
# Log access URLs
logger.info("🌐 Access URLs:")
print("🌐 Access URLs:")
urls = [
("API Health", "http://127.0.0.1:5000/api/health"),
("Admin Panel", "http://localhost:3000/admin/login"),
("Coding Exams", "http://localhost:3000/coding"),
("Real Compiler", "http://localhost:3000/compiler"),
("Join Exam", "http://localhost:3000/coding/join"),
("Wallet Join", "http://localhost:3000/coding/join-wallet")
]
for name, url in urls:
logger.info(f"{name}: {url}")
print(f"{name}: {url}")
# Debug URLs
print("🔧 Debug URLs:")
debug_urls = [
("All Routes", "http://127.0.0.1:5000/debug/routes"),
("Exam Routes", "http://127.0.0.1:5000/debug/exam-routes"),
("Services", "http://127.0.0.1:5000/debug/services"),
("Direct Test", "http://127.0.0.1:5000/api/exam/test-direct")
]
for name, url in debug_urls:
print(f"{name}: {url}")
# Log compiler features
if COMPILER_SERVICE_AVAILABLE:
logger.info("💻 Compiler Features:")
print("💻 Compiler Features:")
features = [
"Multi-language support: Python, Java, C++, C, JavaScript, Go, Rust, Bash",
"Real-time code execution with output capture",
"Secure Docker containerization",
"Resource monitoring and limits"
]
for feature in features:
logger.info(f"{feature}")
print(f"{feature}")
# Log admin token (partially masked)
admin_token = app.config.get('ADMIN_TOKEN', 'admin-secret-key')
if admin_token:
logger.info(f"🔑 Admin token configured: {admin_token[:8]}...")
print(f"🔑 Admin token configured: {admin_token[:8]}...")
return True
except Exception as e:
logger.error(f"❌ Failed to initialize: {str(e)}")
logger.error("Make sure MongoDB is running and accessible")
logger.error(f"❌ Failed to initialize application: {str(e)}")
print(f"❌ Failed to initialize application: {str(e)}")
logger.error("Make sure MongoDB and Docker are running and accessible")
print("Make sure MongoDB and Docker are running and accessible")
return False
# ✅ Enhanced Flask app configuration
if __name__ == '__main__':
# Initialize application
init_success = initialize_application()
if not init_success:
logger.error("❌ Application initialization failed - some features may not work")
print("❌ Application initialization failed - some features may not work")
try:
logger.info("🚀 Starting OpenLearnX Backend Server...")
print("🚀 Starting OpenLearnX Backend Server...")
logger.info("📚 Features: Blockchain Certificates, Coding Exams, Wallet Auth, Real Multi-language Compiler")
print("📚 Features: Blockchain Certificates, Coding Exams, Wallet Auth, Real Multi-language Compiler")
# Start Flask application
app.run(
debug=True,
host='0.0.0.0',
port=5000,
threaded=True # Better for handling multiple requests
threaded=True,
use_reloader=False
)
except KeyboardInterrupt:
logger.info("👋 Server stopped by user")
print("👋 Server stopped by user")
except Exception as e:
logger.error(f"❌ Server startup failed: {str(e)}")
print(f"❌ Server startup failed: {str(e)}")
+225 -73
View File
@@ -1,88 +1,240 @@
from flask import Blueprint, jsonify, request, current_app
import requests
from bson import ObjectId
from flask import Blueprint, request, jsonify, session
from functools import wraps
import subprocess
import tempfile
import os
import time
import uuid
from datetime import datetime
import docker
import psutil
bp = Blueprint('coding', __name__)
PISTON_API_URL = "https://emkc.org/api/v2/piston/execute"
@bp.route("/problems", methods=["GET"])
async def get_problems():
mongo = current_app.config['MONGO_SERVICE']
problems = await mongo.db.coding_problems.find().to_list(100)
for p in problems:
p['_id'] = str(p['_id'])
return jsonify(problems)
@bp.route("/problems/<problem_id>", methods=["GET"])
async def get_problem(problem_id):
mongo = current_app.config['MONGO_SERVICE']
prob = await mongo.db.coding_problems.find_one({"_id": ObjectId(problem_id)})
if not prob:
return jsonify({"error": "Problem not found"}), 404
prob['_id'] = str(prob['_id'])
return jsonify(prob)
@bp.route("/run", methods=["POST"])
async def run_code():
data = request.json
problem_id = data.get("problem_id")
code = data.get("code")
language = data.get("language")
mongo = current_app.config['MONGO_SERVICE']
problem = await mongo.db.coding_problems.find_one({"_id": ObjectId(problem_id)})
if not problem:
return jsonify({"error": "Problem not found"}), 404
# Concatenate all test case inputs
input_data = '\n'.join([tc['input'] for tc in problem['test_cases']])
def secure_execution_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
# Check if user is in secure coding mode
if not session.get('secure_coding_mode'):
return jsonify({"error": "Secure coding mode required"}), 403
return f(*args, **kwargs)
return decorated_function
@bp.route("/start-session", methods=["POST"])
def start_coding_session():
"""Start a secure coding session"""
try:
resp = requests.post(
PISTON_API_URL,
json={
"language": language,
"source": code,
"input": input_data
},
timeout=10,
)
resp.raise_for_status()
result = resp.json()
data = request.json
course_id = data.get('course_id')
lesson_id = data.get('lesson_id')
session_id = str(uuid.uuid4())
session['coding_session_id'] = session_id
session['secure_coding_mode'] = True
session['start_time'] = datetime.now().isoformat()
session['course_id'] = course_id
session['lesson_id'] = lesson_id
return jsonify({
"success": True,
"session_id": session_id,
"message": "Secure coding session started",
"restrictions": {
"copy_paste_disabled": True,
"browser_locked": True,
"extensions_blocked": True,
"virtual_detection": True
}
})
except Exception as e:
return jsonify({"error": str(e)}), 500
# Compare output against expected (simple line-by-line check)
output_lines = result.get("output", "").strip().split('\n')
expected_outputs = [tc['expected_output'].strip() for tc in problem['test_cases']]
correct = output_lines == expected_outputs
@bp.route("/execute", methods=["POST"])
@secure_execution_required
def execute_code():
"""Execute code securely in isolated environment"""
try:
data = request.json
code = data.get('code')
language = data.get('language', 'python')
test_cases = data.get('test_cases', [])
if not code:
return jsonify({"error": "No code provided"}), 400
# Log coding attempt
log_coding_attempt(session['coding_session_id'], code, language)
# Execute code in secure container
result = execute_in_container(code, language, test_cases)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/submit-test", methods=["POST"])
@secure_execution_required
def submit_coding_test():
"""Submit coding test for evaluation"""
try:
data = request.json
code = data.get('code')
problem_id = data.get('problem_id')
# Validate against test cases
test_result = validate_test_submission(code, problem_id)
# Store submission
submission_id = store_submission(
session['coding_session_id'],
session['course_id'],
problem_id,
code,
test_result
)
return jsonify({
"output": result.get("output"),
"error": result.get("stderr"),
"runtime": result.get("stats", {}).get("duration"),
"correct": correct,
"success": True,
"submission_id": submission_id,
"score": test_result['score'],
"passed_tests": test_result['passed'],
"total_tests": test_result['total'],
"feedback": test_result['feedback']
})
except Exception as e:
return jsonify({"error": str(e)}), 500
def execute_in_container(code, language, test_cases):
"""Execute code in secure Docker container"""
try:
client = docker.from_env()
# Language-specific container configuration
containers = {
'python': 'python:3.9-alpine',
'java': 'openjdk:11-alpine',
'javascript': 'node:16-alpine'
}
if language not in containers:
return {"error": "Unsupported language"}
# Create temporary file
with tempfile.NamedTemporaryFile(mode='w', suffix=f'.{get_file_extension(language)}', delete=False) as f:
f.write(code)
temp_file = f.name
try:
# Run container with security restrictions
container = client.containers.run(
containers[language],
command=get_run_command(language, temp_file),
volumes={os.path.dirname(temp_file): {'bind': '/app', 'mode': 'ro'}},
working_dir='/app',
mem_limit='128m',
cpu_period=100000,
cpu_quota=50000, # 50% CPU limit
network_mode='none', # No network access
remove=True,
timeout=10, # 10 second timeout
detach=False
)
output = container.decode('utf-8')
# Run test cases if provided
test_results = []
if test_cases:
for test in test_cases:
test_result = run_test_case(code, language, test)
test_results.append(test_result)
return {
"success": True,
"output": output,
"test_results": test_results,
"execution_time": "< 10s"
}
finally:
os.unlink(temp_file)
except docker.errors.ContainerError as e:
return {"error": f"Runtime error: {e}"}
except docker.errors.ImageNotFound:
return {"error": "Language runtime not available"}
except Exception as e:
return {"error": f"Execution failed: {str(e)}"}
def get_file_extension(language):
extensions = {
'python': 'py',
'java': 'java',
'javascript': 'js'
}
return extensions.get(language, 'txt')
def get_run_command(language, filename):
commands = {
'python': f'python /app/{os.path.basename(filename)}',
'java': f'javac /app/{os.path.basename(filename)} && java -cp /app {os.path.splitext(os.path.basename(filename))[0]}',
'javascript': f'node /app/{os.path.basename(filename)}'
}
return commands.get(language)
def log_coding_attempt(session_id, code, language):
"""Log all coding attempts for monitoring"""
from pymongo import MongoClient
client = MongoClient(os.getenv('MONGODB_URI', 'mongodb://localhost:27017/'))
db = client.openlearnx
db.coding_logs.insert_one({
"session_id": session_id,
"code": code,
"language": language,
"timestamp": datetime.now(),
"ip_address": request.remote_addr,
"user_agent": request.headers.get('User-Agent')
})
@bp.route("/submit", methods=["POST"])
async def submit_solution():
# Same as run_code, but can mark problem as solved
user = await get_authenticated_user()
if not user:
return jsonify({"error": "Unauthorized"}), 401
def validate_test_submission(code, problem_id):
"""Validate code against predefined test cases"""
# Load test cases for the problem
test_cases = get_problem_test_cases(problem_id)
# Run the code first
result = await run_code()
jres = result.get_json()
passed = 0
total = len(test_cases)
feedback = []
if jres.get("correct"):
mongo = current_app.config['MONGO_SERVICE']
# Record that user solved problem
await mongo.db.user_solutions.update_one(
{"user_id": user['_id'], "problem_id": jres.get('problem_id')},
{"$set": {"solved": True, "solved_at": datetime.utcnow()}},
upsert=True
)
return jsonify(jres)
``
for i, test_case in enumerate(test_cases):
result = run_test_case(code, 'python', test_case)
if result['passed']:
passed += 1
feedback.append(f"Test {i+1}: ✅ Passed")
else:
feedback.append(f"Test {i+1}: ❌ Failed - {result['error']}")
score = (passed / total) * 100
return {
"score": score,
"passed": passed,
"total": total,
"feedback": feedback
}
def get_problem_test_cases(problem_id):
"""Get test cases for a specific problem"""
# This would load from your database
test_cases_db = {
"python-basics-1": [
{"input": "hello", "expected_output": "HELLO"},
{"input": "world", "expected_output": "WORLD"}
],
"java-oop-1": [
{"input": "5", "expected_output": "25"},
{"input": "10", "expected_output": "100"}
]
}
return test_cases_db.get(problem_id, [])
+165
View File
@@ -0,0 +1,165 @@
from flask import Blueprint, request, jsonify, session
from services.real_compiler_service import real_compiler_service
import uuid
from datetime import datetime
bp = Blueprint('compiler', __name__)
@bp.route("/languages", methods=["GET"])
def get_supported_languages():
"""Get list of supported programming languages"""
try:
languages = real_compiler_service.get_supported_languages()
return jsonify({
"success": True,
"languages": languages,
"total_languages": len(languages)
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/execute", methods=["POST"])
def execute_code():
"""Execute code and return real output"""
try:
data = request.json
# Validate input
code = data.get('code', '').strip()
language = data.get('language', 'python')
input_data = data.get('input', '')
if not code:
return jsonify({"error": "No code provided"}), 400
if language not in [lang['id'] for lang in real_compiler_service.get_supported_languages()]:
return jsonify({"error": f"Language '{language}' not supported"}), 400
# Generate execution ID
execution_id = str(uuid.uuid4())
# Execute code
result = real_compiler_service.execute_code(
code=code,
language=language,
input_data=input_data,
execution_id=execution_id
)
return jsonify(result)
except Exception as e:
return jsonify({"error": f"Execution failed: {str(e)}"}), 500
@bp.route("/execute-async", methods=["POST"])
def execute_code_async():
"""Start asynchronous code execution"""
try:
data = request.json
execution_id = str(uuid.uuid4())
# Add to execution queue
real_compiler_service.execution_queue.put({
'execution_id': execution_id,
'code': data.get('code'),
'language': data.get('language', 'python'),
'input_data': data.get('input', ''),
'callback_url': data.get('callback_url')
})
return jsonify({
"success": True,
"execution_id": execution_id,
"message": "Code execution started",
"status_url": f"/api/compiler/status/{execution_id}"
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/status/<execution_id>", methods=["GET"])
def get_execution_status(execution_id):
"""Get status of code execution"""
try:
status = real_compiler_service.get_execution_status(execution_id)
if status:
return jsonify({
"success": True,
"execution_id": execution_id,
"status": status['status'],
"start_time": status['start_time'].isoformat(),
"language": status['language']
})
else:
return jsonify({
"success": False,
"error": "Execution not found"
}), 404
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/cancel/<execution_id>", methods=["POST"])
def cancel_execution(execution_id):
"""Cancel a running execution"""
try:
success = real_compiler_service.cancel_execution(execution_id)
return jsonify({
"success": success,
"message": "Execution cancelled" if success else "Execution not found"
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/test", methods=["POST"])
def test_compiler():
"""Test compiler with sample code"""
try:
language = request.json.get('language', 'python')
test_codes = {
'python': 'print("Hello from OpenLearnX Python Compiler!")\nprint("Current time:", __import__("datetime").datetime.now())',
'java': 'public class Main {\n public static void main(String[] args) {\n System.out.println("Hello from OpenLearnX Java Compiler!");\n }\n}',
'cpp': '#include <iostream>\nint main() {\n std::cout << "Hello from OpenLearnX C++ Compiler!" << std::endl;\n return 0;\n}',
'javascript': 'console.log("Hello from OpenLearnX JavaScript Compiler!");',
'go': 'package main\nimport "fmt"\nfunc main() {\n fmt.Println("Hello from OpenLearnX Go Compiler!")\n}',
'rust': 'fn main() {\n println!("Hello from OpenLearnX Rust Compiler!");\n}'
}
test_code = test_codes.get(language, test_codes['python'])
result = real_compiler_service.execute_code(
code=test_code,
language=language,
input_data=""
)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/stats", methods=["GET"])
def get_compiler_stats():
"""Get compiler service statistics"""
try:
active_executions = len(real_compiler_service.active_executions)
queue_size = real_compiler_service.execution_queue.qsize()
supported_languages = len(real_compiler_service.language_configs)
return jsonify({
"success": True,
"stats": {
"active_executions": active_executions,
"queue_size": queue_size,
"supported_languages": supported_languages,
"max_concurrent": real_compiler_service.max_concurrent_executions
},
"uptime": datetime.now().isoformat()
})
except Exception as e:
return jsonify({"error": str(e)}), 500
+515
View File
@@ -0,0 +1,515 @@
from flask import Blueprint, request, jsonify, session
import uuid
import random
import string
from datetime import datetime, timedelta
from pymongo import MongoClient
import os
bp = Blueprint('exam', __name__)
# MongoDB connection
mongo_uri = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/')
client = MongoClient(mongo_uri)
db = client.openlearnx
def generate_exam_code():
"""Generate a unique 6-character exam code"""
while True:
code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
if not db.exams.find_one({"exam_code": code}):
return code
@bp.route("/create-exam", methods=["POST", "OPTIONS"])
def create_exam():
"""Create a new coding exam"""
# Handle OPTIONS request for CORS
if request.method == "OPTIONS":
response = jsonify({'status': 'ok'})
response.headers.add("Access-Control-Allow-Origin", "*")
response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization")
response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS")
return response
try:
print(f"Received create-exam request")
data = request.json
print(f"Request data: {data}")
if not data:
print("❌ No data provided")
return jsonify({"error": "No data provided"}), 400
# Check for basic required fields
if not data.get('title'):
print("❌ Missing title")
return jsonify({"error": "Missing required field: title"}), 400
if not data.get('host_name'):
print("❌ Missing host_name")
return jsonify({"error": "Missing required field: host_name"}), 400
# Handle different problem data formats
problem_title = data.get('problem_title') or data.get('title') or 'Coding Challenge'
problem_description = data.get('problem_description') or f"Solve the {problem_title} problem"
# Handle problem_id if provided
if data.get('problem_id'):
problem_title = problem_title or data.get('problem_id').replace('-', ' ').title()
print(f"Using problem_id: {data.get('problem_id')}")
exam_code = generate_exam_code()
exam = {
"exam_code": exam_code,
"title": data.get('title'),
"host_name": data.get('host_name'),
"created_at": datetime.now(),
"status": "waiting",
"duration_minutes": data.get('duration_minutes', 30),
"max_participants": data.get('max_participants', 50),
"problem": {
"title": problem_title,
"description": problem_description,
"function_name": data.get('function_name', 'solve'),
"languages": data.get('languages', ['python']),
"test_cases": data.get('test_cases', [
{
"input": "hello world",
"expected_output": "Hello World",
"description": "Basic capitalization test"
}
]),
"starter_code": data.get('starter_code', {
'python': 'def solve(input_string):\n # Write your solution here\n return input_string.title()',
'java': 'public String solve(String inputString) {\n // Write your solution here\n return inputString;\n}',
'javascript': 'function solve(inputString) {\n // Write your solution here\n return inputString;\n}'
}),
"constraints": data.get('constraints', ['Input will be a string', 'Length between 1-1000 characters']),
"examples": data.get('examples', [
{
"input": "hello world",
"expected_output": "Hello World",
"description": "Capitalize each word"
}
])
},
"participants": [],
"leaderboard": [],
"start_time": None,
"end_time": None
}
print(f"✅ Creating exam with code: {exam_code}")
print(f"✅ Problem title: {problem_title}")
# Insert into database
result = db.exams.insert_one(exam)
print(f"✅ Exam created successfully with ID: {result.inserted_id}")
return jsonify({
"success": True,
"exam_code": exam_code,
"exam_id": str(result.inserted_id),
"message": f"Exam created successfully! Share code: {exam_code}",
"exam_details": {
"title": exam['title'],
"problem_title": problem_title,
"duration": exam['duration_minutes'],
"max_participants": exam['max_participants'],
"languages": exam['problem']['languages']
}
})
except Exception as e:
print(f"❌ Error creating exam: {str(e)}")
import traceback
traceback.print_exc()
return jsonify({"error": f"Failed to create exam: {str(e)}"}), 500
@bp.route("/join-exam", methods=["POST", "OPTIONS"])
def join_exam():
"""Student joins exam using unique code and their name"""
if request.method == "OPTIONS":
response = jsonify({'status': 'ok'})
response.headers.add("Access-Control-Allow-Origin", "*")
response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization")
response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS")
return response
try:
# Debug logging for the request
print(f"🔍 Raw request data: {request.data}")
print(f"🔍 Content-Type: {request.headers.get('Content-Type')}")
data = request.json
print(f"🔍 Parsed JSON data: {data}")
if not data:
print("❌ No JSON data received")
return jsonify({"error": "No data provided"}), 400
exam_code = data.get('exam_code', '').upper().strip()
student_name = data.get('student_name', '').strip()
print(f"📝 Join exam request - Code: {exam_code}, Name: {student_name}")
# Enhanced validation with detailed error messages
if not exam_code:
print("❌ Missing exam_code")
return jsonify({"error": "Exam code is required"}), 400
if not student_name:
print("❌ Missing student_name")
return jsonify({"error": "Student name is required"}), 400
# Check if exam exists
exam = db.exams.find_one({"exam_code": exam_code})
if not exam:
print(f"❌ Exam not found: {exam_code}")
return jsonify({"error": "Invalid exam code"}), 404
print(f"✅ Found exam: {exam['title']} (Status: {exam['status']})")
# Check exam status
if exam['status'] == 'completed':
print("❌ Exam already completed")
return jsonify({"error": "This exam has already ended"}), 400
# Check capacity
current_participants = exam.get('participants', [])
max_participants = exam.get('max_participants', 50)
if len(current_participants) >= max_participants:
print(f"❌ Exam full: {len(current_participants)}/{max_participants}")
return jsonify({"error": "Exam is full"}), 400
# Check if name is already taken
existing_names = [p['name'].lower() for p in current_participants]
if student_name.lower() in existing_names:
print(f"❌ Name already taken: {student_name}")
return jsonify({"error": "Name already taken. Please choose a different name."}), 400
# Create new participant
participant = {
"name": student_name,
"joined_at": datetime.now(),
"session_id": str(uuid.uuid4()),
"score": 0,
"submission": None,
"language": None,
"submission_time": None,
"completed": False,
"rank": 0,
"test_results": []
}
# Add participant to exam
result = db.exams.update_one(
{"exam_code": exam_code},
{"$push": {"participants": participant}}
)
if result.modified_count == 0:
print("❌ Failed to add participant to database")
return jsonify({"error": "Failed to join exam"}), 500
# Set session data
session['exam_code'] = exam_code
session['student_name'] = student_name
session['session_id'] = participant['session_id']
print(f"✅ Participant {student_name} joined exam {exam_code}")
return jsonify({
"success": True,
"message": f"Successfully joined exam: {exam['title']}",
"exam_info": {
"title": exam['title'],
"duration_minutes": exam['duration_minutes'],
"status": exam['status'],
"participants_count": len(current_participants) + 1,
"max_participants": max_participants,
"languages": exam.get('problem', {}).get('languages', ['python']),
"problem_title": exam.get('problem', {}).get('title', '')
}
})
except Exception as e:
print(f"❌ Error joining exam: {str(e)}")
import traceback
traceback.print_exc()
return jsonify({"error": f"Failed to join exam: {str(e)}"}), 500
@bp.route("/start-exam", methods=["POST", "OPTIONS"])
def start_exam():
"""Host starts the exam"""
if request.method == "OPTIONS":
response = jsonify({'status': 'ok'})
response.headers.add("Access-Control-Allow-Origin", "*")
response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization")
response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS")
return response
try:
data = request.json
exam_code = data.get('exam_code')
print(f"📝 Start exam request - Code: {exam_code}")
exam = db.exams.find_one({"exam_code": exam_code})
if not exam:
return jsonify({"error": "Exam not found"}), 404
if exam['status'] != 'waiting':
return jsonify({"error": "Exam has already started or ended"}), 400
start_time = datetime.now()
end_time = start_time + timedelta(minutes=exam['duration_minutes'])
db.exams.update_one(
{"exam_code": exam_code},
{
"$set": {
"status": "active",
"start_time": start_time,
"end_time": end_time
}
}
)
print(f"✅ Exam {exam_code} started successfully")
return jsonify({
"success": True,
"message": "Exam started successfully!",
"start_time": start_time.isoformat(),
"end_time": end_time.isoformat(),
"participants_count": len(exam.get('participants', []))
})
except Exception as e:
print(f"❌ Error starting exam: {str(e)}")
return jsonify({"error": str(e)}), 500
@bp.route("/leaderboard/<exam_code>", methods=["GET", "OPTIONS"])
def get_leaderboard(exam_code):
"""Get real-time leaderboard visible to all participants"""
if request.method == "OPTIONS":
response = jsonify({'status': 'ok'})
response.headers.add("Access-Control-Allow-Origin", "*")
response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization")
response.headers.add("Access-Control-Allow-Methods", "GET,OPTIONS")
return response
try:
print(f"📝 Leaderboard request - Code: {exam_code}")
exam = db.exams.find_one({"exam_code": exam_code.upper()})
if not exam:
return jsonify({"error": "Exam not found"}), 404
participants = exam.get('participants', [])
# Sort by score and submission time
completed_participants = [p for p in participants if p.get('completed', False)]
leaderboard = sorted(
completed_participants,
key=lambda x: (-x.get('score', 0), x.get('submission_time', datetime.now()))
)
# Add rank to each participant
for i, participant in enumerate(leaderboard):
participant['rank'] = i + 1
waiting_participants = [p for p in participants if not p.get('completed', False)]
# Calculate statistics
total_score = sum(p.get('score', 0) for p in completed_participants)
avg_score = total_score / len(completed_participants) if completed_participants else 0
return jsonify({
"success": True,
"exam_info": {
"title": exam['title'],
"status": exam['status'],
"duration_minutes": exam['duration_minutes'],
"start_time": exam.get('start_time'),
"end_time": exam.get('end_time'),
"problem_title": exam.get('problem', {}).get('title', '')
},
"leaderboard": leaderboard,
"waiting_participants": waiting_participants,
"stats": {
"total_participants": len(participants),
"completed_submissions": len(completed_participants),
"waiting_submissions": len(waiting_participants),
"average_score": round(avg_score, 1),
"highest_score": max((p.get('score', 0) for p in completed_participants), default=0)
}
})
except Exception as e:
print(f"❌ Error getting leaderboard: {str(e)}")
return jsonify({"error": str(e)}), 500
@bp.route("/get-problem/<exam_code>", methods=["GET", "OPTIONS"])
def get_exam_problem(exam_code):
"""Get problem details for participants"""
if request.method == "OPTIONS":
response = jsonify({'status': 'ok'})
response.headers.add("Access-Control-Allow-Origin", "*")
response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization")
response.headers.add("Access-Control-Allow-Methods", "GET,OPTIONS")
return response
try:
exam = db.exams.find_one({"exam_code": exam_code.upper()})
if not exam:
return jsonify({"error": "Exam not found"}), 404
return jsonify({
"success": True,
"problem": exam.get('problem', {}),
"exam_info": {
"title": exam['title'],
"status": exam['status'],
"duration_minutes": exam['duration_minutes']
}
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/host-dashboard/<exam_code>", methods=["GET", "OPTIONS"])
def get_host_dashboard(exam_code):
"""Get comprehensive host dashboard data"""
if request.method == "OPTIONS":
response = jsonify({'status': 'ok'})
response.headers.add("Access-Control-Allow-Origin", "*")
response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization")
response.headers.add("Access-Control-Allow-Methods", "GET,OPTIONS")
return response
try:
exam = db.exams.find_one({"exam_code": exam_code.upper()})
if not exam:
return jsonify({"error": "Exam not found"}), 404
participants = exam.get('participants', [])
# Separate participants by status
completed_participants = [p for p in participants if p.get('completed', False)]
waiting_participants = [p for p in participants if not p.get('completed', False)]
# Sort leaderboard
leaderboard = sorted(
completed_participants,
key=lambda x: (-x.get('score', 0), x.get('submission_time', datetime.now()))
)
# Add ranks
for i, participant in enumerate(leaderboard):
participant['rank'] = i + 1
# Calculate time statistics
current_time = datetime.now()
start_time = exam.get('start_time')
end_time = exam.get('end_time')
time_elapsed = 0
time_remaining = 0
if start_time:
time_elapsed = int((current_time - start_time).total_seconds())
if end_time and current_time < end_time:
time_remaining = int((end_time - current_time).total_seconds())
return jsonify({
"success": True,
"exam_info": {
"exam_code": exam['exam_code'],
"title": exam['title'],
"status": exam['status'],
"duration_minutes": exam['duration_minutes'],
"max_participants": exam.get('max_participants', 50),
"created_at": exam.get('created_at'),
"start_time": start_time,
"end_time": end_time,
"time_elapsed": time_elapsed,
"time_remaining": time_remaining
},
"participants": {
"total": len(participants),
"completed": len(completed_participants),
"working": len(waiting_participants),
"all_participants": sorted(participants, key=lambda x: x.get('joined_at', datetime.now())),
"recent_joins": sorted(participants, key=lambda x: x.get('joined_at', datetime.now()), reverse=True)[:5]
},
"leaderboard": leaderboard,
"statistics": {
"average_score": sum(p.get('score', 0) for p in completed_participants) / len(completed_participants) if completed_participants else 0,
"highest_score": max((p.get('score', 0) for p in completed_participants), default=0),
"lowest_score": min((p.get('score', 0) for p in completed_participants), default=0),
"completion_rate": (len(completed_participants) / len(participants) * 100) if participants else 0
},
"problem": exam.get('problem', {})
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/debug-join-data", methods=["POST", "OPTIONS"])
def debug_join_data():
"""Debug what data is actually being received"""
if request.method == "OPTIONS":
response = jsonify({'status': 'ok'})
response.headers.add("Access-Control-Allow-Origin", "*")
response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization")
response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS")
return response
print(f"🔍 Raw request data: {request.data}")
print(f"🔍 Request JSON: {request.json}")
print(f"🔍 Content-Type: {request.headers.get('Content-Type')}")
return jsonify({
"received_raw": request.data.decode() if request.data else None,
"received_json": request.json,
"content_type": request.headers.get('Content-Type'),
"success": True
})
@bp.route("/test", methods=["GET"])
def test_exam_route():
"""Test if exam routes are working"""
return jsonify({
"success": True,
"message": "Exam routes are working",
"timestamp": datetime.now().isoformat(),
"available_routes": [
"/api/exam/create-exam",
"/api/exam/join-exam",
"/api/exam/start-exam",
"/api/exam/leaderboard/<exam_code>",
"/api/exam/get-problem/<exam_code>",
"/api/exam/host-dashboard/<exam_code>",
"/api/exam/debug-join-data"
]
})
@bp.route("/", methods=["GET"])
def exam_root():
"""Exam route root"""
return jsonify({
"message": "OpenLearnX Exam API",
"available_endpoints": [
"/api/exam/create-exam",
"/api/exam/join-exam",
"/api/exam/start-exam",
"/api/exam/leaderboard/<exam_code>",
"/api/exam/get-problem/<exam_code>",
"/api/exam/host-dashboard/<exam_code>",
"/api/exam/test",
"/api/exam/debug-join-data"
]
})
+42
View File
@@ -0,0 +1,42 @@
import docker
import tempfile
import os # ✅ Make sure this is imported
import subprocess
import time
from typing import Dict, List, Any
import json
class CompilerService:
def __init__(self):
self.client = docker.from_env()
self.language_configs = {
'python': {
'image': 'python:3.9-alpine',
'file_ext': '.py',
'run_command': 'python /app/solution{ext}',
'timeout': 10
},
'java': {
'image': 'openjdk:11-alpine',
'file_ext': '.java',
'run_command': 'cd /app && javac Solution.java && java Solution',
'timeout': 15
},
'c': {
'image': 'gcc:9-alpine',
'file_ext': '.c',
'run_command': 'cd /app && gcc -o solution solution.c && ./solution',
'timeout': 15
},
'bash': {
'image': 'bash:5-alpine',
'file_ext': '.sh',
'run_command': 'bash /app/solution.sh',
'timeout': 10
}
}
# ... rest of your compiler service code
# Global compiler service instance
compiler_service = CompilerService()
+305
View File
@@ -0,0 +1,305 @@
import docker
import tempfile
import os
import subprocess
import time
import uuid
import json
import threading
from typing import Dict, List, Any, Optional
from datetime import datetime
import queue
import signal
class RealCompilerService:
def __init__(self):
self.client = docker.from_env()
self.execution_queue = queue.Queue()
self.active_executions = {}
self.max_concurrent_executions = 5
# Enhanced language configurations with real execution
self.language_configs = {
'python': {
'image': 'python:3.11-slim',
'file_ext': '.py',
'compile_command': None, # Python doesn't need compilation
'run_command': 'python /app/code.py',
'timeout': 30,
'memory_limit': '256m',
'cpu_limit': '0.5'
},
'java': {
'image': 'openjdk:17-alpine',
'file_ext': '.java',
'compile_command': 'javac /app/Main.java',
'run_command': 'java -cp /app Main',
'timeout': 30,
'memory_limit': '512m',
'cpu_limit': '0.5'
},
'cpp': {
'image': 'gcc:latest',
'file_ext': '.cpp',
'compile_command': 'g++ -o /app/program /app/code.cpp -std=c++17',
'run_command': '/app/program',
'timeout': 30,
'memory_limit': '256m',
'cpu_limit': '0.5'
},
'c': {
'image': 'gcc:latest',
'file_ext': '.c',
'compile_command': 'gcc -o /app/program /app/code.c',
'run_command': '/app/program',
'timeout': 30,
'memory_limit': '256m',
'cpu_limit': '0.5'
},
'javascript': {
'image': 'node:18-alpine',
'file_ext': '.js',
'compile_command': None,
'run_command': 'node /app/code.js',
'timeout': 30,
'memory_limit': '256m',
'cpu_limit': '0.5'
},
'bash': {
'image': 'bash:5.2-alpine3.18',
'file_ext': '.sh',
'compile_command': None,
'run_command': 'bash /app/code.sh',
'timeout': 30,
'memory_limit': '128m',
'cpu_limit': '0.3'
},
'go': {
'image': 'golang:1.21-alpine',
'file_ext': '.go',
'compile_command': 'go build -o /app/program /app/code.go',
'run_command': '/app/program',
'timeout': 30,
'memory_limit': '512m',
'cpu_limit': '0.5'
},
'rust': {
'image': 'rust:1.75-alpine',
'file_ext': '.rs',
'compile_command': 'rustc /app/code.rs -o /app/program',
'run_command': '/app/program',
'timeout': 60, # Rust compilation can be slow
'memory_limit': '1g',
'cpu_limit': '1.0'
}
}
# Start execution worker
self.start_execution_worker()
def start_execution_worker(self):
"""Start background worker for code execution"""
def worker():
while True:
try:
execution_task = self.execution_queue.get(timeout=1)
self._execute_task(execution_task)
self.execution_queue.task_done()
except queue.Empty:
continue
except Exception as e:
print(f"Execution worker error: {e}")
worker_thread = threading.Thread(target=worker, daemon=True)
worker_thread.start()
def execute_code(self, code: str, language: str, input_data: str = "",
execution_id: str = None) -> Dict[str, Any]:
"""Execute code with real output capture"""
if language not in self.language_configs:
return {"error": f"Language '{language}' not supported"}
if not execution_id:
execution_id = str(uuid.uuid4())
config = self.language_configs[language]
try:
# Create execution context
execution_context = {
'execution_id': execution_id,
'code': code,
'language': language,
'input_data': input_data,
'config': config,
'start_time': datetime.now(),
'status': 'running'
}
self.active_executions[execution_id] = execution_context
# Execute in Docker container
result = self._execute_in_container(execution_context)
# Update execution context
execution_context['status'] = 'completed'
execution_context['end_time'] = datetime.now()
execution_context['result'] = result
return {
"success": True,
"execution_id": execution_id,
"output": result.get('output', ''),
"error": result.get('error', ''),
"execution_time": result.get('execution_time', 0),
"memory_used": result.get('memory_used', 0),
"exit_code": result.get('exit_code', 0),
"language": language,
"timestamp": datetime.now().isoformat()
}
except Exception as e:
return {
"error": f"Execution failed: {str(e)}",
"execution_id": execution_id,
"language": language
}
finally:
# Clean up
if execution_id in self.active_executions:
del self.active_executions[execution_id]
def _execute_in_container(self, context: Dict) -> Dict[str, Any]:
"""Execute code in secure Docker container"""
code = context['code']
language = context['language']
input_data = context['input_data']
config = context['config']
with tempfile.TemporaryDirectory() as temp_dir:
# Prepare code file
filename = f"code{config['file_ext']}" if language != 'java' else "Main.java"
file_path = os.path.join(temp_dir, filename)
with open(file_path, 'w', encoding='utf-8') as f:
f.write(code)
# Prepare input file
input_file = os.path.join(temp_dir, 'input.txt')
with open(input_file, 'w', encoding='utf-8') as f:
f.write(input_data)
try:
start_time = time.time()
# Create and run container
container = self.client.containers.run(
config['image'],
command=self._build_execution_command(config, filename),
volumes={temp_dir: {'bind': '/app', 'mode': 'rw'}},
working_dir='/app',
mem_limit=config['memory_limit'],
cpu_period=100000,
cpu_quota=int(float(config['cpu_limit']) * 100000),
network_mode='none', # No network access
remove=True,
detach=False,
stdin_open=True,
tty=False,
timeout=config['timeout'],
# Security options
cap_drop=['ALL'],
security_opt=['no-new-privileges'],
read_only=False,
tmpfs={'/tmp': 'rw,noexec,nosuid,size=100m'}
)
execution_time = time.time() - start_time
output = container.decode('utf-8')
return {
"output": output.strip(),
"error": "",
"exit_code": 0,
"execution_time": round(execution_time, 3),
"memory_used": self._get_memory_usage(container)
}
except docker.errors.ContainerError as e:
return {
"output": "",
"error": f"Runtime error (exit code {e.exit_status}): {e.stderr.decode('utf-8') if e.stderr else 'Unknown error'}",
"exit_code": e.exit_status,
"execution_time": time.time() - start_time,
"memory_used": 0
}
except docker.errors.APIError as e:
return {
"output": "",
"error": f"Docker API error: {str(e)}",
"exit_code": -1,
"execution_time": 0,
"memory_used": 0
}
except Exception as e:
return {
"output": "",
"error": f"Execution error: {str(e)}",
"exit_code": -1,
"execution_time": 0,
"memory_used": 0
}
def _build_execution_command(self, config: Dict, filename: str) -> str:
"""Build the execution command for the container"""
commands = []
# Add compilation step if needed
if config.get('compile_command'):
commands.append(config['compile_command'])
# Add execution command with input redirection
run_cmd = config['run_command']
if '<' not in run_cmd: # Add input redirection if not present
run_cmd += ' < /app/input.txt 2>&1'
commands.append(run_cmd)
# Combine commands
return f"sh -c '{' && '.join(commands)}'"
def _get_memory_usage(self, container) -> int:
"""Get memory usage from container stats"""
try:
stats = container.stats(stream=False)
memory_usage = stats['memory']['usage']
return memory_usage
except:
return 0
def get_supported_languages(self) -> List[Dict[str, str]]:
"""Get list of supported languages with details"""
return [
{
'id': lang_id,
'name': lang_id.title(),
'extension': config['file_ext'],
'timeout': config['timeout'],
'memory_limit': config['memory_limit']
}
for lang_id, config in self.language_configs.items()
]
def get_execution_status(self, execution_id: str) -> Optional[Dict]:
"""Get status of a running execution"""
return self.active_executions.get(execution_id)
def cancel_execution(self, execution_id: str) -> bool:
"""Cancel a running execution"""
if execution_id in self.active_executions:
# Implementation would involve stopping the Docker container
del self.active_executions[execution_id]
return True
return False
# Create global instance
real_compiler_service = RealCompilerService()
+53
View File
@@ -0,0 +1,53 @@
from web3 import Web3
from eth_account import Account
from datetime import datetime
import hashlib
import secrets
import os # ✅ Add this missing import
class WalletService:
def __init__(self, web3_provider_url):
self.w3 = Web3(Web3.HTTPProvider(web3_provider_url))
def verify_wallet_signature(self, wallet_address, signature, message):
"""Verify wallet signature for authentication"""
try:
# Recover the address from signature
message_hash = Web3.keccak(text=message)
recovered_address = self.w3.eth.account.recover_message_hash(message_hash, signature=signature)
return recovered_address.lower() == wallet_address.lower()
except Exception as e:
print(f"Signature verification error: {e}")
return False
def generate_auth_message(self, wallet_address, exam_code):
"""Generate message for wallet signing"""
timestamp = int(datetime.now().timestamp())
nonce = secrets.token_hex(16)
message = f"""OpenLearnX Exam Authentication
Wallet: {wallet_address}
Exam Code: {exam_code}
Timestamp: {timestamp}
Nonce: {nonce}
Sign this message to join the coding exam."""
return message, timestamp, nonce
def create_wallet_session(self, wallet_address, exam_code, signature):
"""Create authenticated wallet session"""
session_id = hashlib.sha256(f"{wallet_address}{exam_code}{datetime.now().timestamp()}".encode()).hexdigest()
return {
"session_id": session_id,
"wallet_address": wallet_address,
"exam_code": exam_code,
"authenticated_at": datetime.now(),
"signature": signature
}
# Create the service instance
wallet_service = WalletService(os.getenv('WEB3_PROVIDER_URL', 'http://127.0.0.1:8545'))
+436 -6
View File
@@ -1,11 +1,441 @@
import { CodingProblemView } from "@/components/coding-problem-view"
'use client'
import React, { useState, useEffect } from 'react'
import { useRouter, useParams } from 'next/navigation'
import { Play, Clock, CheckCircle, XCircle, ArrowLeft, Trophy } from 'lucide-react'
interface CodingProblemPageProps {
params: {
problemId: string
interface TestCase {
input: string
expected: string
description: string
}
interface Problem {
id: string
title: string
description: string
difficulty: 'Easy' | 'Medium' | 'Hard'
category: string
examples: TestCase[]
constraints: string[]
hints: string[]
starter_code: string
function_name: string
}
export default function ProblemPage() {
const params = useParams()
const router = useRouter()
const problemId = params.problemId as string
const [problem, setProblem] = useState<Problem | null>(null)
const [code, setCode] = useState('')
const [output, setOutput] = useState('')
const [testResults, setTestResults] = useState<any[]>([])
const [isRunning, setIsRunning] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [showHints, setShowHints] = useState(false)
const [activeTab, setActiveTab] = useState<'description' | 'examples' | 'constraints'>('description')
useEffect(() => {
loadProblem(problemId)
}, [problemId])
const loadProblem = async (id: string) => {
try {
// In a real app, this would fetch from your backend
const problems: Record<string, Problem> = {
'string-capitalizer': {
id: 'string-capitalizer',
title: 'String Capitalizer',
description: 'Write a function that takes a string as input and returns the string converted to uppercase.',
difficulty: 'Easy',
category: 'String Manipulation',
examples: [
{ input: 'hello', expected: 'HELLO', description: 'Basic string conversion' },
{ input: 'world', expected: 'WORLD', description: 'Another basic case' },
{ input: 'Python Programming', expected: 'PYTHON PROGRAMMING', description: 'String with spaces' }
],
constraints: [
'Input string length will be between 1 and 1000 characters',
'Input may contain letters, numbers, and spaces',
'Function must be named exactly "capitalize_string"'
],
hints: [
'Python has a built-in method to convert strings to uppercase',
'The upper() method can be used on any string',
'Remember to return the result, not just print it'
],
starter_code: 'def capitalize_string(text):\n # Write your solution here\n pass',
function_name: 'capitalize_string'
},
'reverse-string': {
id: 'reverse-string',
title: 'Reverse String',
description: 'Write a function that takes a string and returns it reversed.',
difficulty: 'Easy',
category: 'String Manipulation',
examples: [
{ input: 'hello', expected: 'olleh', description: 'Basic string reversal' },
{ input: 'python', expected: 'nohtyp', description: 'Another basic case' },
{ input: 'OpenLearnX', expected: 'XnraeLnepO', description: 'Mixed case string' }
],
constraints: [
'Input string length will be between 1 and 1000 characters',
'Function must be named exactly "reverse_string"'
],
hints: [
'Python strings can be sliced with [::-1]',
'You can also use the reversed() function',
'Remember to return the result'
],
starter_code: 'def reverse_string(text):\n # Write your solution here\n pass',
function_name: 'reverse_string'
},
'fibonacci': {
id: 'fibonacci',
title: 'Fibonacci Sequence',
description: 'Write a function that returns the nth number in the Fibonacci sequence.',
difficulty: 'Medium',
category: 'Algorithms',
examples: [
{ input: '0', expected: '0', description: 'First Fibonacci number' },
{ input: '1', expected: '1', description: 'Second Fibonacci number' },
{ input: '5', expected: '5', description: 'Sixth Fibonacci number (0,1,1,2,3,5)' }
],
constraints: [
'n will be between 0 and 30',
'Function must be named exactly "fibonacci"',
'Should handle edge cases for n=0 and n=1'
],
hints: [
'Base cases: fib(0) = 0, fib(1) = 1',
'For n > 1: fib(n) = fib(n-1) + fib(n-2)',
'Consider using iteration instead of recursion for better performance'
],
starter_code: 'def fibonacci(n):\n # Write your solution here\n pass',
function_name: 'fibonacci'
}
}
export default function CodingProblemPage({ params }: CodingProblemPageProps) {
return <CodingProblemView problemId={params.problemId} />
const selectedProblem = problems[id]
if (selectedProblem) {
setProblem(selectedProblem)
setCode(selectedProblem.starter_code)
} else {
// Problem not found
router.push('/coding')
}
} catch (error) {
console.error('Failed to load problem:', error)
router.push('/coding')
}
}
const runCode = async () => {
if (!problem || !code.trim()) return
setIsRunning(true)
setOutput('')
setTestResults([])
try {
const response = await fetch('http://127.0.0.1:5000/api/coding/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code,
language: 'python',
problem_id: problem.id,
test_cases: problem.examples
})
})
const result = await response.json()
if (result.success) {
setOutput(result.output || 'Code executed successfully')
setTestResults(result.test_results || [])
} else {
setOutput(`Error: ${result.error}`)
}
} catch (error) {
setOutput(`Execution failed: ${(error as Error).message}`)
} finally {
setIsRunning(false)
}
}
const submitSolution = async () => {
if (!problem || !code.trim()) return
setIsSubmitting(true)
try {
const response = await fetch('http://127.0.0.1:5000/api/coding/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code,
problem_id: problem.id
})
})
const result = await response.json()
if (result.success) {
alert(`Solution submitted! Score: ${result.score}% (${result.passed_tests}/${result.total_tests} tests passed)`)
} else {
alert(`Submission failed: ${result.error}`)
}
} catch (error) {
alert('Failed to submit solution')
} finally {
setIsSubmitting(false)
}
}
const getDifficultyColor = (difficulty: string) => {
switch (difficulty) {
case 'Easy': return 'text-green-600 bg-green-100'
case 'Medium': return 'text-yellow-600 bg-yellow-100'
case 'Hard': return 'text-red-600 bg-red-100'
default: return 'text-gray-600 bg-gray-100'
}
}
if (!problem) {
return (
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-2 text-gray-400">Loading problem...</p>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gray-900 text-white">
{/* Header */}
<div className="bg-gray-800 border-b border-gray-700 p-4">
<div className="max-w-7xl mx-auto flex items-center justify-between">
<div className="flex items-center space-x-4">
<button
onClick={() => router.back()}
className="p-2 hover:bg-gray-700 rounded-lg transition-colors"
>
<ArrowLeft className="h-5 w-5" />
</button>
<div>
<h1 className="text-2xl font-bold">{problem.title}</h1>
<div className="flex items-center space-x-3 mt-1">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getDifficultyColor(problem.difficulty)}`}>
{problem.difficulty}
</span>
<span className="text-gray-400 text-sm">{problem.category}</span>
</div>
</div>
</div>
<div className="flex items-center space-x-3">
<button
onClick={() => setShowHints(!showHints)}
className="px-4 py-2 bg-yellow-600 hover:bg-yellow-700 rounded-lg text-sm transition-colors"
>
{showHints ? 'Hide Hints' : 'Show Hints'}
</button>
<button
onClick={() => router.push('/coding/exam')}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg text-sm transition-colors flex items-center space-x-2"
>
<Trophy className="h-4 w-4" />
<span>Join Exam</span>
</button>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto p-6 grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Problem Description */}
<div className="space-y-6">
{/* Navigation Tabs */}
<div className="bg-gray-800 rounded-lg">
<div className="flex border-b border-gray-700">
{(['description', 'examples', 'constraints'] as const).map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-6 py-3 font-medium capitalize transition-colors ${
activeTab === tab
? 'bg-gray-700 text-white border-b-2 border-blue-500'
: 'text-gray-400 hover:text-white'
}`}
>
{tab}
</button>
))}
</div>
<div className="p-6">
{activeTab === 'description' && (
<div className="prose prose-invert max-w-none">
<p className="text-gray-300 leading-relaxed">{problem.description}</p>
</div>
)}
{activeTab === 'examples' && (
<div className="space-y-4">
<h3 className="text-lg font-semibold">Examples:</h3>
{problem.examples.map((example, index) => (
<div key={index} className="bg-gray-900 p-4 rounded-lg">
<div className="mb-2">
<span className="text-blue-400">Input:</span>
<code className="ml-2 text-green-400">"{example.input}"</code>
</div>
<div className="mb-2">
<span className="text-blue-400">Output:</span>
<code className="ml-2 text-green-400">"{example.expected}"</code>
</div>
<div className="text-gray-400 text-sm">{example.description}</div>
</div>
))}
</div>
)}
{activeTab === 'constraints' && (
<div className="space-y-4">
<h3 className="text-lg font-semibold">Constraints:</h3>
<ul className="space-y-2">
{problem.constraints.map((constraint, index) => (
<li key={index} className="flex items-start space-x-2">
<span className="text-blue-400 mt-1"></span>
<span className="text-gray-300">{constraint}</span>
</li>
))}
</ul>
</div>
)}
</div>
</div>
{/* Hints Section */}
{showHints && (
<div className="bg-yellow-900 border border-yellow-600 rounded-lg p-6">
<h3 className="text-lg font-semibold mb-4 text-yellow-300">💡 Hints:</h3>
<ul className="space-y-2">
{problem.hints.map((hint, index) => (
<li key={index} className="flex items-start space-x-2">
<span className="text-yellow-400 mt-1">{index + 1}.</span>
<span className="text-yellow-100">{hint}</span>
</li>
))}
</ul>
</div>
)}
</div>
{/* Code Editor & Results */}
<div className="space-y-6">
{/* Code Editor */}
<div className="bg-gray-800 rounded-lg p-6">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-bold">Code Editor</h3>
<span className="text-sm text-gray-400">Python</span>
</div>
<textarea
value={code}
onChange={(e) => setCode(e.target.value)}
className="w-full h-80 bg-gray-900 text-green-400 font-mono p-4 rounded border border-gray-600 resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
spellCheck={false}
/>
<div className="flex justify-between items-center mt-4">
<div className="text-sm text-gray-400">
Function: <code className="text-blue-400">{problem.function_name}</code>
</div>
<div className="flex space-x-3">
<button
onClick={runCode}
disabled={isRunning || !code.trim()}
className="bg-green-600 hover:bg-green-700 disabled:bg-gray-600 px-4 py-2 rounded flex items-center space-x-2 transition-colors"
>
<Play className="h-4 w-4" />
<span>{isRunning ? 'Running...' : 'Run Code'}</span>
</button>
<button
onClick={submitSolution}
disabled={isSubmitting || !code.trim()}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 px-4 py-2 rounded flex items-center space-x-2 transition-colors"
>
<CheckCircle className="h-4 w-4" />
<span>{isSubmitting ? 'Submitting...' : 'Submit'}</span>
</button>
</div>
</div>
</div>
{/* Output & Test Results */}
<div className="bg-gray-800 rounded-lg p-6">
<h3 className="text-lg font-bold mb-4">Output & Test Results</h3>
{/* Console Output */}
{output && (
<div className="mb-4">
<h4 className="text-sm font-medium text-gray-400 mb-2">Console Output:</h4>
<div className="bg-black p-4 rounded font-mono text-sm">
<pre className="text-green-400 whitespace-pre-wrap">{output}</pre>
</div>
</div>
)}
{/* Test Results */}
{testResults.length > 0 && (
<div>
<h4 className="text-sm font-medium text-gray-400 mb-2">Test Results:</h4>
<div className="space-y-2">
{testResults.map((result, index) => (
<div
key={index}
className={`p-3 rounded flex items-center justify-between ${
result.passed ? 'bg-green-900 border border-green-600' : 'bg-red-900 border border-red-600'
}`}
>
<div className="flex items-center space-x-2">
{result.passed ? (
<CheckCircle className="h-4 w-4 text-green-400" />
) : (
<XCircle className="h-4 w-4 text-red-400" />
)}
<span className="text-sm">Test {index + 1}</span>
</div>
<div className="text-right text-sm">
{result.passed ? (
<span className="text-green-400">Passed</span>
) : (
<span className="text-red-400">Failed: {result.error}</span>
)}
</div>
</div>
))}
</div>
</div>
)}
{!output && testResults.length === 0 && (
<div className="text-center text-gray-400 py-8">
<Clock className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p>Run your code to see output and test results</p>
</div>
)}
</div>
</div>
</div>
</div>
)
}
+312
View File
@@ -0,0 +1,312 @@
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
export default function CreateExam() {
const [examData, setExamData] = useState({
title: 'String Capitalizer Challenge',
host_name: '',
duration_minutes: 30,
max_participants: 50,
problem_id: 'string-capitalizer'
})
const [loading, setLoading] = useState(false)
const [result, setResult] = useState('')
const [error, setError] = useState('')
const router = useRouter()
const handleInputChange = (field: string, value: any) => {
setExamData(prev => ({
...prev,
[field]: value
}))
setError('')
}
const createExam = async () => {
// Clear previous messages
setError('')
setResult('')
// Validation
if (!examData.title.trim()) {
setError('Please enter exam title')
return
}
if (!examData.host_name.trim()) {
setError('Please enter host name')
return
}
setLoading(true)
setResult('⏳ Creating exam...')
try {
const payload = {
title: examData.title.trim(),
problem_id: examData.problem_id,
duration_minutes: examData.duration_minutes,
host_name: examData.host_name.trim(),
max_participants: examData.max_participants
}
console.log('📤 Sending payload:', payload)
const response = await fetch('http://127.0.0.1:5000/api/exam/create-exam', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(payload)
})
console.log('📡 Response status:', response.status)
const data = await response.json()
console.log('📦 Full backend response:', data)
if (data.success) {
// ✅ ENHANCED DEBUGGING - Log all fields
console.log('🔍 All response fields:', Object.keys(data))
console.log('📝 exam_code field:', data.exam_code)
console.log('🗄️ exam_id field:', data.exam_id)
console.log('📋 exam_details:', data.exam_details)
// ✅ CORRECTED: Use exam_code, NOT exam_id
const participantCode = data.exam_code // This should be "JEX99M"
const databaseId = data.exam_id // This is the MongoDB ObjectId
console.log('📝 Participant Code (CORRECT for sharing):', participantCode)
console.log('🗄️ Database ID (internal only):', databaseId)
// ✅ ENHANCED: Check if exam_code exists
if (!participantCode) {
console.error('❌ ERROR: exam_code is missing from response!')
setError('Backend did not return exam_code. Check backend logs.')
return
}
// ✅ SIMPLIFIED SUCCESS MESSAGE - Easier to spot issues
const simpleAlert = `Exam created! Share this code with participants: ${participantCode}`
// ✅ DETAILED SUCCESS MESSAGE for result display
const successMessage = `🎉 EXAM CREATED SUCCESSFULLY!
📝 EXAM CODE FOR PARTICIPANTS:
┌─────────────────┐
${participantCode}
└─────────────────┘
📋 Exam Details:
• Title: ${data.exam_details?.title || examData.title}
• Duration: ${data.exam_details?.duration || examData.duration_minutes} minutes
• Max Participants: ${data.exam_details?.max_participants || examData.max_participants}
• Host: ${examData.host_name}
• Languages: ${data.exam_details?.languages?.join(', ') || 'Python'}
🔗 Share this code with participants: ${participantCode}
📱 Join URL: localhost:3000/coding/join
⚠️ IMPORTANT: Give participants "${participantCode}",
NOT the database ID "${databaseId}"!
✅ Participants will use: ${participantCode}`
setResult(successMessage)
// ✅ SIMPLE ALERT - This should show the correct code
alert(simpleAlert)
// ✅ ADDITIONAL PROMINENT ALERT
setTimeout(() => {
alert(`✅ EXAM CODE: ${participantCode}
Share this 6-character code with participants.
They will enter: ${participantCode}`)
}, 500)
// Store the correct exam code for host dashboard
localStorage.setItem('created_exam', JSON.stringify({
exam_code: participantCode, // 6-character code for participants
exam_id: databaseId, // Internal database ID
exam_details: data.exam_details,
host_name: examData.host_name,
created_at: new Date().toISOString()
}))
// Redirect to host dashboard after 5 seconds (increased time)
setTimeout(() => {
router.push(`/coding/host/${participantCode}`)
}, 5000)
} else {
setError(data.error || 'Failed to create exam')
setResult('')
}
} catch (error) {
console.error('❌ Network error:', error)
setError('Network error: Could not connect to backend server')
setResult('')
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen bg-gradient-to-br from-green-900 via-blue-900 to-purple-900 flex items-center justify-center p-4">
<div className="bg-white rounded-xl shadow-2xl p-8 w-full max-w-2xl">
{/* Header */}
<div className="text-center mb-8">
<div className="w-16 h-16 bg-gradient-to-r from-green-600 to-blue-600 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="h-8 w-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4" />
</svg>
</div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">Create Coding Exam</h1>
<p className="text-gray-600">Set up a new coding challenge for participants</p>
</div>
{/* Form */}
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Exam Title
</label>
<input
type="text"
value={examData.title}
onChange={(e) => handleInputChange('title', e.target.value)}
placeholder="Enter exam title"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Host/Instructor Name
</label>
<input
type="text"
value={examData.host_name}
onChange={(e) => handleInputChange('host_name', e.target.value)}
placeholder="Enter your name"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
required
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Duration (minutes)
</label>
<input
type="number"
value={examData.duration_minutes}
onChange={(e) => handleInputChange('duration_minutes', parseInt(e.target.value) || 30)}
min="5"
max="180"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Max Participants
</label>
<input
type="number"
value={examData.max_participants}
onChange={(e) => handleInputChange('max_participants', parseInt(e.target.value) || 50)}
min="1"
max="200"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
</div>
</div>
<button
onClick={createExam}
disabled={loading || !examData.title.trim() || !examData.host_name.trim()}
className="w-full bg-gradient-to-r from-green-600 to-blue-600 hover:from-green-700 hover:to-blue-700 text-white font-semibold py-3 px-4 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
{loading ? (
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent mr-2"></div>
Creating Exam...
</div>
) : (
<>
<svg className="inline h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4" />
</svg>
Create Exam
</>
)}
</button>
</div>
{/* Error Display */}
{error && (
<div className="mt-6 bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg text-sm flex items-start">
<svg className="h-4 w-4 mr-2 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{error}
</div>
)}
{/* Success Result Display */}
{result && (
<div className="mt-6 p-6 bg-green-50 border border-green-200 text-green-700 rounded-lg whitespace-pre-line text-sm">
{result}
</div>
)}
{/* Enhanced Debug Info */}
<div className="mt-6 p-4 bg-gray-50 rounded-lg text-xs">
<p className="text-gray-500 mb-2">Debug Info:</p>
<p className="text-gray-400">Title: "{examData.title}"</p>
<p className="text-gray-400">Host: "{examData.host_name}"</p>
<p className="text-gray-400">Duration: {examData.duration_minutes} minutes</p>
<p className="text-green-600 font-medium"> Will show exam_code (6 chars), NOT exam_id</p>
<p className="text-blue-600 font-medium">🔍 Check browser console for detailed logs</p>
</div>
{/* Features Info */}
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-medium text-gray-700 mb-3">Exam Features:</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm text-gray-600">
<div className="flex items-center">
<svg className="h-4 w-4 text-green-500 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Real-time participant tracking
</div>
<div className="flex items-center">
<svg className="h-4 w-4 text-blue-500 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Live leaderboard
</div>
<div className="flex items-center">
<svg className="h-4 w-4 text-purple-500 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m18 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
Multi-language support
</div>
<div className="flex items-center">
<svg className="h-4 w-4 text-yellow-500 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Timed exam sessions
</div>
</div>
</div>
</div>
</div>
)
}
+547
View File
@@ -0,0 +1,547 @@
'use client'
import React, { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { Trophy, Clock, Users, Send, RefreshCw, Play, Code, Wallet, Shield } from 'lucide-react'
interface Participant {
name: string
score: number
rank: number
completed: boolean
language?: string
submission_time?: string
wallet_address?: string
wallet_short?: string
blockchain_verified?: boolean
}
interface Problem {
title: string
description: string
function_name: string
languages: string[]
examples: Array<{input: string, expected_output: string, description: string}>
constraints: string[]
starter_code: {[key: string]: string}
}
interface ExamSession {
exam_code: string
student_name: string
wallet_address?: string
blockchain_verified?: boolean
exam_info: any
}
export default function EnhancedExamInterface() {
const [examSession, setExamSession] = useState<ExamSession | null>(null)
const [problem, setProblem] = useState<Problem | null>(null)
const [selectedLanguage, setSelectedLanguage] = useState('python')
const [code, setCode] = useState('')
const [output, setOutput] = useState('')
const [testResults, setTestResults] = useState<any[]>([])
const [leaderboard, setLeaderboard] = useState<Participant[]>([])
const [waitingParticipants, setWaitingParticipants] = useState<Participant[]>([])
const [timeRemaining, setTimeRemaining] = useState(0)
const [isRunning, setIsRunning] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [hasSubmitted, setHasSubmitted] = useState(false)
const [examStats, setExamStats] = useState<any>({})
const router = useRouter()
const languageIcons: {[key: string]: string} = {
python: '🐍',
java: '☕',
c: '⚡',
bash: '💻'
}
useEffect(() => {
const sessionData = localStorage.getItem('exam_session')
if (!sessionData) {
router.push('/coding/join')
return
}
const session = JSON.parse(sessionData)
setExamSession(session)
// Fetch problem details
fetchProblem(session.exam_code)
// Start polling for updates
const interval = setInterval(() => {
fetchLeaderboard(session.exam_code)
}, 3000)
return () => clearInterval(interval)
}, [router])
const fetchProblem = async (examCode: string) => {
try {
const response = await fetch(`http://127.0.0.1:5000/api/exam/get-problem/${examCode}`)
const data = await response.json()
if (data.success) {
setProblem(data.problem)
const defaultLang = data.problem.languages[0] || 'python'
setSelectedLanguage(defaultLang)
setCode(data.problem.starter_code[defaultLang] || '')
}
} catch (error) {
console.error('Failed to fetch problem:', error)
}
}
const fetchLeaderboard = async (examCode: string) => {
try {
const response = await fetch(`http://127.0.0.1:5000/api/exam/leaderboard/${examCode}`)
const data = await response.json()
if (data.success) {
setLeaderboard(data.leaderboard || [])
setWaitingParticipants(data.waiting_participants || [])
setExamStats(data.stats || {})
if (data.exam_info.status === 'active' && data.exam_info.end_time) {
const endTime = new Date(data.exam_info.end_time)
const now = new Date()
const remaining = Math.max(0, Math.floor((endTime.getTime() - now.getTime()) / 1000))
setTimeRemaining(remaining)
}
}
} catch (error) {
console.error('Failed to fetch leaderboard:', error)
}
}
const handleLanguageChange = (language: string) => {
setSelectedLanguage(language)
if (problem?.starter_code[language]) {
setCode(problem.starter_code[language])
}
setOutput('')
setTestResults([])
}
const runCode = async () => {
if (!code.trim()) {
alert('Please write some code first!')
return
}
setIsRunning(true)
setOutput('')
setTestResults([])
try {
const response = await fetch('http://127.0.0.1:5000/api/exam/execute-code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code,
language: selectedLanguage
})
})
const result = await response.json()
if (result.success) {
setOutput('Code executed successfully!')
setTestResults(result.test_results || [])
} else {
setOutput(`Error: ${result.error}`)
}
} catch (error) {
setOutput(`Execution failed: ${(error as Error).message}`)
} finally {
setIsRunning(false)
}
}
const submitSolution = async () => {
if (!code.trim()) {
alert('Please write some code before submitting!')
return
}
setIsSubmitting(true)
try {
const response = await fetch('http://127.0.0.1:5000/api/exam/submit-solution', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code,
language: selectedLanguage
})
})
const data = await response.json()
if (data.success) {
setHasSubmitted(true)
setTestResults(data.test_results || [])
let alertMessage = `Solution submitted successfully!\nScore: ${data.score}%\nPassed: ${data.passed_tests}/${data.total_tests} tests`
if (data.blockchain_verified) {
alertMessage += `\n🔗 Blockchain Verified: ${data.wallet_address?.slice(0, 6)}...${data.wallet_address?.slice(-4)}`
}
alert(alertMessage)
fetchLeaderboard(examSession!.exam_code)
} else {
alert(data.error || 'Failed to submit solution')
}
} catch (error) {
alert('Failed to submit solution. Please try again.')
} finally {
setIsSubmitting(false)
}
}
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
const getRankColor = (rank: number) => {
switch (rank) {
case 1: return 'bg-gradient-to-r from-yellow-400 to-yellow-600 text-white'
case 2: return 'bg-gradient-to-r from-gray-300 to-gray-500 text-white'
case 3: return 'bg-gradient-to-r from-orange-400 to-orange-600 text-white'
default: return 'bg-gray-100 text-gray-700'
}
}
if (!examSession || !problem) {
return (
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-2 text-gray-400">Loading exam interface...</p>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gray-900 text-white">
{/* Header with Timer */}
<div className="bg-gray-800 border-b border-gray-700 p-4">
<div className="max-w-7xl mx-auto flex justify-between items-center">
<div>
<h1 className="text-xl font-bold">{problem.title}</h1>
<p className="text-gray-400">Code: {examSession.exam_code}</p>
</div>
<div className="flex items-center space-x-4">
{/* Timer */}
{timeRemaining > 0 && (
<div className="flex items-center space-x-2 bg-red-900 px-3 py-1 rounded-lg">
<Clock className="h-5 w-5 text-red-400" />
<span className="font-mono text-lg text-red-400">{formatTime(timeRemaining)}</span>
</div>
)}
{/* Wallet Info Display */}
{examSession.blockchain_verified && examSession.wallet_address && (
<div className="flex items-center space-x-2 bg-green-900 px-3 py-1 rounded-lg">
<Wallet className="h-4 w-4 text-green-400" />
<span className="text-green-200 text-sm font-mono">
{examSession.wallet_address.slice(0, 6)}...{examSession.wallet_address.slice(-4)}
</span>
<Shield className="h-4 w-4 text-green-400" />
</div>
)}
{/* Participant Count */}
<div className="flex items-center space-x-2">
<Users className="h-5 w-5 text-blue-400" />
<span>{examStats.total_participants || 0} participants</span>
{examStats.blockchain_participants > 0 && (
<span className="text-green-400 text-sm">
({examStats.blockchain_participants} 🔗)
</span>
)}
</div>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto p-6 grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Problem & Code Editor */}
<div className="lg:col-span-2 space-y-6">
{/* Problem Description */}
<div className="bg-gray-800 rounded-lg p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold">{problem.title}</h2>
{examSession.blockchain_verified && (
<div className="flex items-center space-x-1 text-green-400 text-sm">
<Shield className="h-4 w-4" />
<span>Blockchain Verified</span>
</div>
)}
</div>
<div className="prose prose-invert">
<p className="mb-4 text-gray-300">{problem.description}</p>
<h4 className="text-lg font-semibold mb-2">Examples:</h4>
{problem.examples.map((example, index) => (
<div key={index} className="bg-gray-900 p-4 rounded mb-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div>
<span className="text-blue-400">Input:</span>
<code className="ml-2 text-green-400">"{example.input}"</code>
</div>
<div>
<span className="text-blue-400">Output:</span>
<code className="ml-2 text-green-400">"{example.expected_output}"</code>
</div>
</div>
{example.description && (
<div className="mt-2 text-gray-400 text-sm">{example.description}</div>
)}
</div>
))}
<h4 className="text-lg font-semibold mb-2">Constraints:</h4>
<ul className="list-disc list-inside mb-4 text-gray-300">
{problem.constraints.map((constraint, index) => (
<li key={index}>{constraint}</li>
))}
</ul>
</div>
</div>
{/* Code Editor */}
<div className="bg-gray-800 rounded-lg p-6">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-bold">Your Solution</h3>
{/* Language Selector */}
<div className="flex items-center space-x-2">
<Code className="h-4 w-4 text-gray-400" />
<select
value={selectedLanguage}
onChange={(e) => handleLanguageChange(e.target.value)}
disabled={hasSubmitted}
className="bg-gray-700 text-white px-3 py-1 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500"
>
{problem.languages.map(lang => (
<option key={lang} value={lang}>
{languageIcons[lang]} {lang.charAt(0).toUpperCase() + lang.slice(1)}
</option>
))}
</select>
</div>
</div>
<textarea
value={code}
onChange={(e) => setCode(e.target.value)}
className="w-full h-64 bg-gray-900 text-green-400 font-mono p-4 rounded border border-gray-600 resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
disabled={hasSubmitted}
spellCheck={false}
placeholder={`Write your ${selectedLanguage} solution here...`}
/>
<div className="flex justify-between items-center mt-4">
<div className="text-sm text-gray-400">
Function: <code className="text-blue-400">{problem.function_name}</code>
{hasSubmitted && (
<span className="ml-4 text-green-400">
Solution submitted
{examSession.blockchain_verified && (
<span className="ml-2">🔗 Blockchain verified</span>
)}
</span>
)}
</div>
<div className="flex space-x-3">
<button
onClick={runCode}
disabled={isRunning || hasSubmitted || !code.trim()}
className="bg-green-600 hover:bg-green-700 disabled:bg-gray-600 px-4 py-2 rounded flex items-center space-x-2 transition-colors"
>
<Play className="h-4 w-4" />
<span>{isRunning ? 'Running...' : 'Test Code'}</span>
</button>
<button
onClick={submitSolution}
disabled={isSubmitting || hasSubmitted || !code.trim()}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 px-4 py-2 rounded flex items-center space-x-2 transition-colors"
>
<Send className="h-4 w-4" />
<span>{isSubmitting ? 'Submitting...' : hasSubmitted ? 'Submitted' : 'Submit Solution'}</span>
</button>
</div>
</div>
{/* Output & Test Results */}
{(output || testResults.length > 0) && (
<div className="mt-6 bg-gray-900 p-4 rounded">
{output && (
<div className="mb-4">
<h4 className="text-sm font-medium text-gray-400 mb-2">Output:</h4>
<pre className="text-green-400 text-sm whitespace-pre-wrap">{output}</pre>
</div>
)}
{testResults.length > 0 && (
<div>
<h4 className="text-sm font-medium text-gray-400 mb-2">Test Results:</h4>
<div className="space-y-2">
{testResults.map((result, index) => (
<div
key={index}
className={`p-3 rounded text-sm ${
result.passed ? 'bg-green-900 text-green-200' : 'bg-red-900 text-red-200'
}`}
>
<div className="flex justify-between items-start">
<div>
<span className="font-medium">
Test {index + 1}: {result.passed ? '✅ Passed' : '❌ Failed'}
</span>
{result.input && (
<div className="text-xs mt-1 opacity-75">
Input: "{result.input}"
</div>
)}
</div>
{!result.passed && result.error && (
<span className="text-xs text-right">{result.error}</span>
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
</div>
{/* Leaderboard */}
<div className="bg-gray-800 rounded-lg p-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-2">
<Trophy className="h-6 w-6 text-yellow-400" />
<h3 className="text-xl font-bold">Live Leaderboard</h3>
</div>
<button
onClick={() => fetchLeaderboard(examSession.exam_code)}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded"
title="Refresh"
>
<RefreshCw className="h-4 w-4" />
</button>
</div>
{/* Stats */}
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="bg-gray-900 p-3 rounded">
<div className="text-2xl font-bold text-blue-400">{examStats.completed_submissions || 0}</div>
<div className="text-xs text-gray-400">Submitted</div>
</div>
<div className="bg-gray-900 p-3 rounded">
<div className="text-2xl font-bold text-green-400">{Math.round(examStats.average_score || 0)}%</div>
<div className="text-xs text-gray-400">Avg Score</div>
</div>
<div className="bg-gray-900 p-3 rounded">
<div className="text-2xl font-bold text-purple-400">{examStats.highest_score || 0}%</div>
<div className="text-xs text-gray-400">Top Score</div>
</div>
<div className="bg-gray-900 p-3 rounded">
<div className="text-2xl font-bold text-orange-400">{examStats.waiting_submissions || 0}</div>
<div className="text-xs text-gray-400">Working</div>
</div>
</div>
{/* Blockchain Stats */}
{examStats.blockchain_participants > 0 && (
<div className="bg-green-900 p-3 rounded mb-6">
<div className="flex items-center justify-between">
<div>
<div className="text-lg font-bold text-green-200">{examStats.blockchain_participants}</div>
<div className="text-xs text-green-300">Blockchain Verified</div>
</div>
<Shield className="h-6 w-6 text-green-400" />
</div>
</div>
)}
{/* Leaderboard */}
<div className="space-y-2">
<h4 className="font-semibold text-gray-300 mb-3">🏆 Rankings</h4>
{leaderboard.length > 0 ? (
leaderboard.map((participant) => (
<div key={participant.name} className={`p-3 rounded-lg ${getRankColor(participant.rank)}`}>
<div className="flex justify-between items-center">
<div className="flex items-center space-x-3">
<span className="font-bold text-lg">#{participant.rank}</span>
<div>
<div className={`font-medium ${participant.name === examSession.student_name ? 'underline' : ''}`}>
{participant.name}
{participant.name === examSession.student_name && ' (You)'}
{participant.blockchain_verified && (
<Shield className="inline h-3 w-3 ml-1 text-green-400" />
)}
</div>
<div className="text-xs opacity-75 flex items-center space-x-2">
{participant.language && (
<span>
{languageIcons[participant.language]} {participant.language}
</span>
)}
{participant.wallet_short && (
<span className="font-mono text-green-300">
{participant.wallet_short}
</span>
)}
</div>
</div>
</div>
<span className="font-bold text-lg">{participant.score}%</span>
</div>
</div>
))
) : (
<div className="text-center text-gray-400 py-4">
No submissions yet
</div>
)}
</div>
{/* Waiting Participants */}
{waitingParticipants.length > 0 && (
<div className="mt-6">
<h4 className="font-semibold text-gray-300 mb-3"> Still Working</h4>
<div className="space-y-1">
{waitingParticipants.map((participant) => (
<div key={participant.name} className="p-2 bg-gray-700 rounded text-sm flex items-center justify-between">
<span>
{participant.name}
{participant.name === examSession.student_name && ' (You)'}
</span>
{participant.blockchain_verified && (
<Shield className="h-3 w-3 text-green-400" />
)}
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
)
}
@@ -0,0 +1,597 @@
'use client'
import React, { useState, useEffect } from 'react'
import { useParams, useRouter } from 'next/navigation'
import {
Users, Trophy, Clock, Play, Square, UserX, AlertTriangle,
RefreshCw, Settings, BarChart, Eye, Trash2, Plus, Timer
} from 'lucide-react'
interface Participant {
name: string
joined_at: string
score: number
completed: boolean
language?: string
submission_time?: string
rank?: number
kicked?: boolean
}
interface ExamData {
exam_info: {
exam_code: string
title: string
status: string
duration_minutes: number
max_participants: number
time_elapsed: number
time_remaining: number
start_time?: string
end_time?: string
}
participants: {
total: number
completed: number
working: number
all_participants: Participant[]
recent_joins: Participant[]
}
leaderboard: Participant[]
statistics: {
average_score: number
highest_score: number
lowest_score: number
completion_rate: number
}
}
export default function HostDashboard() {
const params = useParams()
const router = useRouter()
const examCode = params.examCode as string
const [examData, setExamData] = useState<ExamData | null>(null)
const [loading, setLoading] = useState(true)
const [activeTab, setActiveTab] = useState<'overview' | 'participants' | 'leaderboard' | 'settings'>('overview')
const [selectedParticipant, setSelectedParticipant] = useState<string | null>(null)
const [showKickModal, setShowKickModal] = useState(false)
const [refreshInterval, setRefreshInterval] = useState(3000) // 3 seconds
useEffect(() => {
if (!examCode) return
fetchDashboardData()
const interval = setInterval(fetchDashboardData, refreshInterval)
return () => clearInterval(interval)
}, [examCode, refreshInterval])
const fetchDashboardData = async () => {
try {
const response = await fetch(`http://127.0.0.1:5000/api/exam/host-dashboard/${examCode}`)
const data = await response.json()
if (data.success) {
setExamData(data)
} else {
console.error('Failed to fetch dashboard data:', data.error)
}
} catch (error) {
console.error('Error fetching dashboard data:', error)
} finally {
setLoading(false)
}
}
const startExam = async () => {
try {
const response = await fetch('http://127.0.0.1:5000/api/exam/start-exam', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ exam_code: examCode })
})
const data = await response.json()
if (data.success) {
alert('Exam started successfully!')
fetchDashboardData()
} else {
alert(`Failed to start exam: ${data.error}`)
}
} catch (error) {
alert('Failed to start exam')
}
}
const endExam = async () => {
if (!confirm('Are you sure you want to end the exam? This cannot be undone.')) return
try {
const response = await fetch('http://127.0.0.1:5000/api/exam/end-exam', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ exam_code: examCode })
})
const data = await response.json()
if (data.success) {
alert('Exam ended successfully!')
fetchDashboardData()
} else {
alert(`Failed to end exam: ${data.error}`)
}
} catch (error) {
alert('Failed to end exam')
}
}
const extendExam = async (minutes: number) => {
try {
const response = await fetch('http://127.0.0.1:5000/api/exam/extend-exam', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
exam_code: examCode,
additional_minutes: minutes
})
})
const data = await response.json()
if (data.success) {
alert(`Exam extended by ${minutes} minutes!`)
fetchDashboardData()
} else {
alert(`Failed to extend exam: ${data.error}`)
}
} catch (error) {
alert('Failed to extend exam')
}
}
const removeParticipant = async (participantName: string) => {
if (!confirm(`Are you sure you want to remove "${participantName}" from the exam?`)) return
try {
const response = await fetch('http://127.0.0.1:5000/api/exam/remove-participant', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
exam_code: examCode,
participant_name: participantName
})
})
const data = await response.json()
if (data.success) {
alert(`Participant "${participantName}" removed successfully!`)
fetchDashboardData()
setShowKickModal(false)
setSelectedParticipant(null)
} else {
alert(`Failed to remove participant: ${data.error}`)
}
} catch (error) {
alert('Failed to remove participant')
}
}
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
const getStatusColor = (status: string) => {
switch (status) {
case 'waiting': return 'bg-yellow-100 text-yellow-800'
case 'active': return 'bg-green-100 text-green-800'
case 'completed': return 'bg-gray-100 text-gray-800'
default: return 'bg-gray-100 text-gray-800'
}
}
if (loading) {
return (
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-2 text-gray-400">Loading host dashboard...</p>
</div>
</div>
)
}
if (!examData) {
return (
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
<div className="text-center">
<AlertTriangle className="h-12 w-12 text-red-400 mx-auto mb-4" />
<h2 className="text-xl font-bold mb-2">Exam Not Found</h2>
<p className="text-gray-400">The exam code "{examCode}" is invalid or expired.</p>
<button
onClick={() => router.push('/coding/create')}
className="mt-4 bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded"
>
Create New Exam
</button>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gray-900 text-white">
{/* Header */}
<div className="bg-gray-800 border-b border-gray-700 p-6">
<div className="max-w-7xl mx-auto">
<div className="flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold">{examData.exam_info.title}</h1>
<div className="flex items-center space-x-4 mt-2">
<span className="text-lg font-mono font-bold text-blue-400">
CODE: {examData.exam_info.exam_code}
</span>
<span className={`px-3 py-1 rounded-full text-sm font-medium ${getStatusColor(examData.exam_info.status)}`}>
{examData.exam_info.status.toUpperCase()}
</span>
<span className="text-gray-400">
{examData.participants.total}/{examData.exam_info.max_participants} participants
</span>
</div>
</div>
<div className="flex items-center space-x-4">
{/* Timer */}
{examData.exam_info.status === 'active' && examData.exam_info.time_remaining > 0 && (
<div className="flex items-center space-x-2 bg-red-900 px-4 py-2 rounded-lg">
<Clock className="h-5 w-5 text-red-400" />
<span className="font-mono text-lg">{formatTime(examData.exam_info.time_remaining)}</span>
</div>
)}
{/* Control Buttons */}
{examData.exam_info.status === 'waiting' && (
<button
onClick={startExam}
className="bg-green-600 hover:bg-green-700 px-4 py-2 rounded-lg flex items-center space-x-2"
>
<Play className="h-4 w-4" />
<span>Start Exam</span>
</button>
)}
{examData.exam_info.status === 'active' && (
<div className="flex space-x-2">
<button
onClick={() => extendExam(10)}
className="bg-yellow-600 hover:bg-yellow-700 px-3 py-2 rounded flex items-center space-x-1"
>
<Timer className="h-4 w-4" />
<span>+10min</span>
</button>
<button
onClick={endExam}
className="bg-red-600 hover:bg-red-700 px-4 py-2 rounded-lg flex items-center space-x-2"
>
<Square className="h-4 w-4" />
<span>End Exam</span>
</button>
</div>
)}
<button
onClick={fetchDashboardData}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded"
title="Refresh"
>
<RefreshCw className="h-4 w-4" />
</button>
</div>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto p-6">
{/* Tab Navigation */}
<div className="flex space-x-1 mb-6">
{[
{ id: 'overview', label: 'Overview', icon: BarChart },
{ id: 'participants', label: 'Participants', icon: Users },
{ id: 'leaderboard', label: 'Leaderboard', icon: Trophy },
{ id: 'settings', label: 'Settings', icon: Settings }
].map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`px-4 py-2 rounded-lg flex items-center space-x-2 transition-colors ${
activeTab === tab.id
? 'bg-blue-600 text-white'
: 'bg-gray-800 text-gray-400 hover:text-white hover:bg-gray-700'
}`}
>
<tab.icon className="h-4 w-4" />
<span>{tab.label}</span>
</button>
))}
</div>
{/* Tab Content */}
{activeTab === 'overview' && (
<div className="space-y-6">
{/* Statistics Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="bg-gray-800 p-6 rounded-lg">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-gray-400">Total Participants</h3>
<p className="text-3xl font-bold text-blue-400">{examData.participants.total}</p>
</div>
<Users className="h-8 w-8 text-blue-400" />
</div>
</div>
<div className="bg-gray-800 p-6 rounded-lg">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-gray-400">Completed</h3>
<p className="text-3xl font-bold text-green-400">{examData.participants.completed}</p>
</div>
<Trophy className="h-8 w-8 text-green-400" />
</div>
</div>
<div className="bg-gray-800 p-6 rounded-lg">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-gray-400">Still Working</h3>
<p className="text-3xl font-bold text-yellow-400">{examData.participants.working}</p>
</div>
<Clock className="h-8 w-8 text-yellow-400" />
</div>
</div>
<div className="bg-gray-800 p-6 rounded-lg">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-gray-400">Average Score</h3>
<p className="text-3xl font-bold text-purple-400">
{Math.round(examData.statistics.average_score)}%
</p>
</div>
<BarChart className="h-8 w-8 text-purple-400" />
</div>
</div>
</div>
{/* Recent Activity */}
<div className="bg-gray-800 rounded-lg p-6">
<h3 className="text-lg font-bold mb-4">Recent Participants</h3>
<div className="space-y-2">
{examData.participants.recent_joins.slice(0, 5).map((participant, index) => (
<div key={participant.name} className="flex items-center justify-between p-3 bg-gray-700 rounded">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center text-sm font-bold">
{participant.name.charAt(0).toUpperCase()}
</div>
<div>
<div className="font-medium">{participant.name}</div>
<div className="text-sm text-gray-400">
Joined {new Date(participant.joined_at).toLocaleTimeString()}
</div>
</div>
</div>
<div className="text-right">
<div className={`px-2 py-1 rounded text-xs ${
participant.completed ? 'bg-green-900 text-green-200' : 'bg-yellow-900 text-yellow-200'
}`}>
{participant.completed ? `${participant.score}% completed` : 'Working'}
</div>
</div>
</div>
))}
</div>
</div>
</div>
)}
{activeTab === 'participants' && (
<div className="bg-gray-800 rounded-lg overflow-hidden">
<div className="p-6 border-b border-gray-700">
<div className="flex justify-between items-center">
<h3 className="text-lg font-bold">All Participants ({examData.participants.total})</h3>
<div className="text-sm text-gray-400">
Completion Rate: {Math.round(examData.statistics.completion_rate)}%
</div>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Participant</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Score</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Language</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Joined</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-700">
{examData.participants.all_participants.map((participant) => (
<tr key={participant.name} className="hover:bg-gray-700">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center text-sm font-bold mr-3">
{participant.name.charAt(0).toUpperCase()}
</div>
<div className="font-medium">{participant.name}</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
participant.completed
? 'bg-green-900 text-green-200'
: participant.kicked
? 'bg-red-900 text-red-200'
: 'bg-yellow-900 text-yellow-200'
}`}>
{participant.kicked ? 'Kicked' : participant.completed ? 'Completed' : 'Working'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-300">
{participant.completed ? `${participant.score}%` : '-'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-300">
{participant.language || '-'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-300">
{new Date(participant.joined_at).toLocaleString()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex space-x-2">
<button
onClick={() => {
setSelectedParticipant(participant.name)
setShowKickModal(true)
}}
className="text-red-400 hover:text-red-300"
title="Remove Participant"
>
<UserX className="h-4 w-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{activeTab === 'leaderboard' && (
<div className="bg-gray-800 rounded-lg p-6">
<h3 className="text-lg font-bold mb-6">Live Leaderboard</h3>
<div className="space-y-3">
{examData.leaderboard.map((participant, index) => {
const rankColors = {
1: 'bg-gradient-to-r from-yellow-600 to-yellow-500 text-white',
2: 'bg-gradient-to-r from-gray-400 to-gray-500 text-white',
3: 'bg-gradient-to-r from-orange-600 to-orange-500 text-white'
}
return (
<div
key={participant.name}
className={`p-4 rounded-lg ${
rankColors[participant.rank as keyof typeof rankColors] || 'bg-gray-700'
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className="text-2xl font-bold">#{participant.rank}</div>
<div>
<div className="font-bold text-lg">{participant.name}</div>
<div className="text-sm opacity-75">
{participant.language && `${participant.language}`}
Submitted: {new Date(participant.submission_time!).toLocaleTimeString()}
</div>
</div>
</div>
<div className="text-right">
<div className="text-2xl font-bold">{participant.score}%</div>
</div>
</div>
</div>
)
})}
</div>
</div>
)}
{activeTab === 'settings' && (
<div className="space-y-6">
<div className="bg-gray-800 rounded-lg p-6">
<h3 className="text-lg font-bold mb-4">Exam Controls</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<button
onClick={() => extendExam(5)}
disabled={examData.exam_info.status !== 'active'}
className="bg-yellow-600 hover:bg-yellow-700 disabled:bg-gray-600 px-4 py-2 rounded text-left"
>
<div className="font-medium">Extend by 5 minutes</div>
<div className="text-sm opacity-75">Add more time to the exam</div>
</button>
<button
onClick={() => extendExam(15)}
disabled={examData.exam_info.status !== 'active'}
className="bg-yellow-600 hover:bg-yellow-700 disabled:bg-gray-600 px-4 py-2 rounded text-left"
>
<div className="font-medium">Extend by 15 minutes</div>
<div className="text-sm opacity-75">Add significant extra time</div>
</button>
<button
onClick={endExam}
disabled={examData.exam_info.status !== 'active'}
className="bg-red-600 hover:bg-red-700 disabled:bg-gray-600 px-4 py-2 rounded text-left"
>
<div className="font-medium">End Exam Early</div>
<div className="text-sm opacity-75">Stop the exam immediately</div>
</button>
</div>
</div>
<div className="bg-gray-800 rounded-lg p-6">
<h3 className="text-lg font-bold mb-4">Auto-Refresh Settings</h3>
<div className="flex items-center space-x-4">
<label className="text-sm font-medium">Update Interval:</label>
<select
value={refreshInterval}
onChange={(e) => setRefreshInterval(Number(e.target.value))}
className="bg-gray-700 border border-gray-600 rounded px-3 py-1 text-sm"
>
<option value={1000}>1 second</option>
<option value={3000}>3 seconds</option>
<option value={5000}>5 seconds</option>
<option value={10000}>10 seconds</option>
</select>
</div>
</div>
</div>
)}
</div>
{/* Kick Participant Modal */}
{showKickModal && selectedParticipant && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-gray-800 rounded-lg p-6 w-full max-w-md">
<h3 className="text-lg font-bold mb-4">Remove Participant</h3>
<p className="text-gray-300 mb-6">
Are you sure you want to remove <strong>"{selectedParticipant}"</strong> from the exam?
This action cannot be undone.
</p>
<div className="flex justify-end space-x-3">
<button
onClick={() => {
setShowKickModal(false)
setSelectedParticipant(null)
}}
className="px-4 py-2 bg-gray-600 hover:bg-gray-700 rounded"
>
Cancel
</button>
<button
onClick={() => removeParticipant(selectedParticipant)}
className="px-4 py-2 bg-red-600 hover:bg-red-700 rounded"
>
Remove Participant
</button>
</div>
</div>
</div>
)}
</div>
)
}
+330
View File
@@ -0,0 +1,330 @@
'use client'
import React, { useState } from 'react'
import { useRouter } from 'next/navigation'
import { Wallet, Shield, Code, AlertCircle, CheckCircle } from 'lucide-react'
declare global {
interface Window {
ethereum?: any
}
}
export default function JoinExamWallet() {
const [examCode, setExamCode] = useState('')
const [studentName, setStudentName] = useState('')
const [walletAddress, setWalletAddress] = useState('')
const [isConnecting, setIsConnecting] = useState(false)
const [isJoining, setIsJoining] = useState(false)
const [error, setError] = useState('')
const [step, setStep] = useState<'connect' | 'auth' | 'join'>('connect')
const [authMessage, setAuthMessage] = useState('')
const router = useRouter()
const connectWallet = async () => {
if (!window.ethereum) {
setError('MetaMask is not installed. Please install MetaMask to continue.')
return
}
setIsConnecting(true)
setError('')
try {
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
})
if (accounts.length > 0) {
setWalletAddress(accounts[0])
setStep('auth')
}
} catch (error: any) {
setError('Failed to connect wallet: ' + error.message)
} finally {
setIsConnecting(false)
}
}
const getAuthMessage = async () => {
if (!examCode.trim()) {
setError('Please enter exam code')
return
}
try {
const response = await fetch('http://127.0.0.1:5000/api/exam/wallet-auth-message', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
wallet_address: walletAddress,
exam_code: examCode.toUpperCase()
})
})
const data = await response.json()
if (data.success) {
setAuthMessage(data.message)
setStep('join')
} else {
setError(data.error || 'Failed to get authentication message')
}
} catch (error) {
setError('Connection failed. Please check if the backend is running.')
}
}
const signAndJoinExam = async () => {
if (!studentName.trim()) {
setError('Please enter your name')
return
}
setIsJoining(true)
setError('')
try {
// Sign the message
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [authMessage, walletAddress]
})
// Join exam with signature
const response = await fetch('http://127.0.0.1:5000/api/exam/join-exam-wallet', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
wallet_address: walletAddress,
exam_code: examCode.toUpperCase(),
signature: signature,
student_name: studentName
})
})
const data = await response.json()
if (data.success) {
// Store session data
localStorage.setItem('exam_session', JSON.stringify({
exam_code: examCode.toUpperCase(),
student_name: studentName,
wallet_address: walletAddress,
blockchain_verified: true,
exam_info: data.exam_info
}))
// Redirect to exam interface
router.push('/coding/exam')
} else {
setError(data.error || 'Failed to join exam')
}
} catch (error: any) {
if (error.code === 4001) {
setError('Transaction was cancelled by user')
} else {
setError('Failed to sign message or join exam')
}
} finally {
setIsJoining(false)
}
}
return (
<div className="min-h-screen bg-gradient-to-br from-blue-900 via-purple-900 to-pink-900 flex items-center justify-center p-4">
<div className="bg-white rounded-xl shadow-2xl p-8 w-full max-w-lg">
{/* Header */}
<div className="text-center mb-8">
<div className="w-16 h-16 bg-gradient-to-r from-blue-600 to-purple-600 rounded-full flex items-center justify-center mx-auto mb-4">
<Wallet className="h-8 w-8 text-white" />
</div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">Join with Wallet</h1>
<p className="text-gray-600">Connect your wallet to join the blockchain-verified coding exam</p>
</div>
{/* Step Indicator */}
<div className="flex items-center justify-center mb-8">
<div className="flex items-center space-x-2">
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
step === 'connect' ? 'bg-blue-600 text-white' :
walletAddress ? 'bg-green-600 text-white' : 'bg-gray-300 text-gray-600'
}`}>
1
</div>
<div className="w-12 h-1 bg-gray-300"></div>
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
step === 'auth' ? 'bg-blue-600 text-white' :
authMessage ? 'bg-green-600 text-white' : 'bg-gray-300 text-gray-600'
}`}>
2
</div>
<div className="w-12 h-1 bg-gray-300"></div>
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
step === 'join' ? 'bg-blue-600 text-white' : 'bg-gray-300 text-gray-600'
}`}>
3
</div>
</div>
</div>
{/* Step 1: Connect Wallet */}
{step === 'connect' && (
<div className="space-y-6">
<div className="text-center">
<Shield className="h-12 w-12 text-blue-600 mx-auto mb-4" />
<h2 className="text-xl font-bold mb-2">Connect Your Wallet</h2>
<p className="text-gray-600 mb-6">Connect MetaMask to verify your identity on the blockchain</p>
</div>
{walletAddress ? (
<div className="bg-green-50 border border-green-200 p-4 rounded-lg">
<div className="flex items-center">
<CheckCircle className="h-5 w-5 text-green-600 mr-2" />
<span className="font-medium text-green-800">Wallet Connected</span>
</div>
<p className="text-sm text-green-700 mt-1 font-mono">
{walletAddress.slice(0, 6)}...{walletAddress.slice(-4)}
</p>
<button
onClick={() => setStep('auth')}
className="mt-3 w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg"
>
Continue
</button>
</div>
) : (
<button
onClick={connectWallet}
disabled={isConnecting}
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white font-semibold py-3 px-4 rounded-lg disabled:opacity-50"
>
{isConnecting ? (
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent mr-2"></div>
Connecting...
</div>
) : (
<>
<Wallet className="inline h-5 w-5 mr-2" />
Connect MetaMask
</>
)}
</button>
)}
</div>
)}
{/* Step 2: Get Auth Message */}
{step === 'auth' && (
<div className="space-y-6">
<div className="text-center">
<Code className="h-12 w-12 text-purple-600 mx-auto mb-4" />
<h2 className="text-xl font-bold mb-2">Enter Exam Code</h2>
<p className="text-gray-600 mb-6">Enter the exam code provided by your instructor</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Exam Code
</label>
<input
type="text"
value={examCode}
onChange={(e) => setExamCode(e.target.value.toUpperCase())}
placeholder="Enter 6-character code"
maxLength={6}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-center text-lg font-mono tracking-widest uppercase"
/>
</div>
<button
onClick={getAuthMessage}
disabled={!examCode.trim()}
className="w-full bg-purple-600 hover:bg-purple-700 disabled:bg-gray-400 text-white font-semibold py-3 px-4 rounded-lg"
>
Get Authentication Message
</button>
</div>
)}
{/* Step 3: Sign and Join */}
{step === 'join' && (
<div className="space-y-6">
<div className="text-center">
<Shield className="h-12 w-12 text-green-600 mx-auto mb-4" />
<h2 className="text-xl font-bold mb-2">Sign & Join Exam</h2>
<p className="text-gray-600 mb-6">Enter your name and sign the message to join</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Your Name
</label>
<input
type="text"
value={studentName}
onChange={(e) => setStudentName(e.target.value)}
placeholder="Enter your full name"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
{authMessage && (
<div className="bg-gray-50 border border-gray-200 p-4 rounded-lg">
<p className="text-sm text-gray-600 mb-2">You will sign this message:</p>
<div className="bg-white p-3 rounded border text-xs font-mono text-gray-800 max-h-32 overflow-y-auto">
{authMessage}
</div>
</div>
)}
<button
onClick={signAndJoinExam}
disabled={isJoining || !studentName.trim()}
className="w-full bg-gradient-to-r from-green-600 to-blue-600 hover:from-green-700 hover:to-blue-700 text-white font-semibold py-3 px-4 rounded-lg disabled:opacity-50"
>
{isJoining ? (
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent mr-2"></div>
Signing & Joining...
</div>
) : (
<>
<Shield className="inline h-5 w-5 mr-2" />
Sign & Join Exam
</>
)}
</button>
</div>
)}
{/* Error Display */}
{error && (
<div className="mt-6 bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg text-sm flex items-start">
<AlertCircle className="h-4 w-4 mr-2 mt-0.5 flex-shrink-0" />
{error}
</div>
)}
{/* Features */}
<div className="mt-8 pt-6 border-t border-gray-200">
<h3 className="text-sm font-medium text-gray-700 mb-3">Blockchain Benefits:</h3>
<div className="space-y-2 text-sm text-gray-600">
<div className="flex items-center">
<Shield className="h-4 w-4 text-green-500 mr-2" />
Tamper-proof identity verification
</div>
<div className="flex items-center">
<Wallet className="h-4 w-4 text-blue-500 mr-2" />
Wallet-based authentication
</div>
<div className="flex items-center">
<Code className="h-4 w-4 text-purple-500 mr-2" />
Permanent participation records
</div>
</div>
</div>
</div>
</div>
)
}
+183
View File
@@ -0,0 +1,183 @@
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
export default function JoinExam() {
const [examCode, setExamCode] = useState('')
const [studentName, setStudentName] = useState('')
const [loading, setLoading] = useState(false)
const [result, setResult] = useState('')
const router = useRouter()
const join = async () => {
if (!examCode || !studentName) {
setResult('❌ Please fill both fields')
return
}
setLoading(true)
setResult('⏳ Joining exam...')
try {
const payload = {
exam_code: examCode.trim().toUpperCase(),
student_name: studentName.trim()
}
console.log('🚀 Sending:', payload)
const response = await fetch('http://127.0.0.1:5000/api/exam/join-exam', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
const data = await response.json()
console.log('📦 Response:', data)
if (data.success) {
// ✅ ENHANCED SUCCESS DISPLAY
const successMessage = `✅ Successfully joined: ${data.exam_info.title}
📋 Exam Details:
• Status: ${data.exam_info.status}
• Duration: ${data.exam_info.duration_minutes} minutes
• Participants: ${data.exam_info.participants_count}/${data.exam_info.max_participants}
• Languages: ${data.exam_info.languages.join(', ')}
• Problem: ${data.exam_info.problem_title}
🎯 You're now registered for the exam!
⏳ Wait for the host to start the exam.`
setResult(successMessage)
// Store session data
localStorage.setItem('exam_session', JSON.stringify({
exam_code: examCode.toUpperCase(),
student_name: studentName,
exam_info: data.exam_info,
joined_at: new Date().toISOString()
}))
// Show success alert
alert(`🎉 Welcome to the exam!
📝 Exam: ${data.exam_info.title}
👤 Joined as: ${studentName}
📊 You are participant #${data.exam_info.participants_count}
✅ Successfully registered!`)
// Redirect to exam waiting page after 2 seconds
setTimeout(() => {
router.push('/coding/exam')
}, 2000)
} else {
setResult(`❌ Error: ${data.error}`)
}
} catch (error) {
console.error('❌ Error:', error)
setResult('❌ Network error: Could not connect to server')
} finally {
setLoading(false)
}
}
return (
<div style={{ padding: '50px', background: '#1a1a1a', color: 'white', minHeight: '100vh', fontFamily: 'monospace' }}>
<h1>🚀 Join Coding Exam</h1>
<div style={{ maxWidth: '500px', marginTop: '30px' }}>
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '5px', color: '#4CAF50' }}>
Exam Code:
</label>
<input
value={examCode}
onChange={e => setExamCode(e.target.value)}
placeholder="0C3LQ8"
style={{
width: '100%',
padding: '12px',
background: '#333',
color: 'white',
border: '2px solid #4CAF50',
borderRadius: '4px',
fontFamily: 'monospace',
fontSize: '16px',
textTransform: 'uppercase'
}}
/>
</div>
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '5px', color: '#4CAF50' }}>
Your Name:
</label>
<input
value={studentName}
onChange={e => setStudentName(e.target.value)}
placeholder="Your name"
style={{
width: '100%',
padding: '12px',
background: '#333',
color: 'white',
border: '2px solid #4CAF50',
borderRadius: '4px',
fontSize: '16px'
}}
/>
</div>
<button
onClick={join}
disabled={loading}
style={{
width: '100%',
padding: '15px',
background: loading ? '#666' : '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px',
fontSize: '18px',
cursor: loading ? 'not-allowed' : 'pointer',
fontWeight: 'bold'
}}
>
{loading ? '⏳ Joining Exam...' : '🚀 Join Exam'}
</button>
</div>
{/* ENHANCED RESULT DISPLAY */}
{result && (
<div style={{
marginTop: '30px',
padding: '20px',
background: result.includes('✅') ? '#1a4a1a' : '#4a1a1a',
border: result.includes('✅') ? '2px solid #4CAF50' : '2px solid #f44336',
borderRadius: '8px',
whiteSpace: 'pre-line',
fontSize: '14px',
lineHeight: '1.6'
}}>
{result}
</div>
)}
<div style={{
marginTop: '30px',
padding: '15px',
background: '#333',
borderRadius: '4px',
border: '2px solid #4CAF50'
}}>
<h3 style={{ color: '#4CAF50' }}>🔧 Debug Info:</h3>
<p>Exam Code: "{examCode}"</p>
<p>Student Name: "{studentName}"</p>
<p style={{ color: '#4CAF50' }}> Backend working correctly</p>
</div>
</div>
)
}
+452 -3
View File
@@ -1,5 +1,454 @@
import { CodingProblemList } from "@/components/coding-problem-list"
'use client'
import React, { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { Play, Lock, Shield, AlertTriangle, Users, Trophy, Clock } from 'lucide-react'
export default function CodingPage() {
return <CodingProblemList />
type UserRole = 'selector' | 'host' | 'participant'
type ExamStatus = 'waiting' | 'active' | 'completed'
interface Participant {
name: string
score: number
completed: boolean
submitted_at?: string
}
export default function CodingExamPlatform() {
const [userRole, setUserRole] = useState<UserRole>('selector')
const [examId, setExamId] = useState('')
const [participantName, setParticipantName] = useState('')
const [examInfo, setExamInfo] = useState<any>(null)
const [systemChecked, setSystemChecked] = useState(false)
const [isSecureMode, setIsSecureMode] = useState(false)
const [leaderboard, setLeaderboard] = useState<Participant[]>([])
const [timeRemaining, setTimeRemaining] = useState(0)
// Coding states
const [code, setCode] = useState('')
const [output, setOutput] = useState('')
const [isExecuting, setIsExecuting] = useState(false)
const router = useRouter()
// System Requirements Check
const checkSystemRequirements = () => {
const checks = {
fullscreenSupported: document.fullscreenEnabled,
webGLSupported: !!document.createElement('canvas').getContext('webgl'),
localStorageSupported: typeof Storage !== 'undefined',
cookiesEnabled: navigator.cookieEnabled
}
const allPassed = Object.values(checks).every(check => check)
if (allPassed) {
setSystemChecked(true)
alert('System requirements check passed! ✅')
} else {
alert('System requirements not met. Please use a modern browser.')
}
return allPassed
}
const acceptSystemRequirements = () => {
if (checkSystemRequirements()) {
enableSecureMode()
}
}
const enableSecureMode = () => {
// Enter fullscreen
document.documentElement.requestFullscreen().then(() => {
setIsSecureMode(true)
disableBrowserFeatures()
detectVirtualEnvironment()
// Start exam timer if in active exam
if (examInfo?.status === 'active') {
startExamTimer()
}
}).catch(() => {
alert('Fullscreen mode is required for secure coding')
})
}
const disableBrowserFeatures = () => {
// Disable right-click, copy/paste, dev tools
const blockActions = (e: KeyboardEvent) => {
if (e.ctrlKey && ['c', 'v', 'x', 'a'].includes(e.key)) {
e.preventDefault()
alert('Copy/paste is disabled in exam mode')
}
if (e.key === 'F12' || (e.ctrlKey && e.shiftKey && ['I', 'C'].includes(e.key))) {
e.preventDefault()
alert('Developer tools are disabled')
}
}
document.addEventListener('keydown', blockActions)
document.addEventListener('contextmenu', e => e.preventDefault())
document.addEventListener('selectstart', e => e.preventDefault())
}
const detectVirtualEnvironment = () => {
const canvas = document.createElement('canvas')
const gl = canvas.getContext('webgl')
if (gl) {
const renderer = gl.getParameter(gl.RENDERER)
if (renderer.includes('VMware') || renderer.includes('VirtualBox')) {
alert('Virtual environment detected. Exam will be terminated.')
window.location.href = '/'
}
}
}
const createExam = async () => {
try {
const response = await fetch('http://127.0.0.1:5000/api/exam/create-exam', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: 'String Capitalizer Challenge',
problem_id: 'string-capitalizer',
duration_minutes: 30,
host_name: participantName
})
})
const data = await response.json()
if (data.success) {
setExamId(data.exam_code)
setExamInfo({ title: 'String Capitalizer Challenge', status: 'waiting' })
alert(`Exam created! Share this code with participants: ${data.exam_code}`)
}
} catch (error) {
alert('Failed to create exam')
}
}
const joinExam = async () => {
try {
const response = await fetch('http://127.0.0.1:5000/api/exam/join-exam', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
exam_id: examId,
name: participantName
})
})
const data = await response.json()
if (data.success) {
setExamInfo(data.exam_info)
alert('Successfully joined the exam!')
} else {
alert(data.error)
}
} catch (error) {
alert('Failed to join exam')
}
}
const startExam = async () => {
try {
const response = await fetch('http://127.0.0.1:5000/api/exam/start-exam', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ exam_id: examId })
})
const data = await response.json()
if (data.success) {
setExamInfo(prev => ({ ...prev, status: 'active' }))
alert('Exam started! Participants can now begin coding.')
startExamTimer()
}
} catch (error) {
alert('Failed to start exam')
}
}
const startExamTimer = () => {
const duration = 30 * 60 // 30 minutes in seconds
setTimeRemaining(duration)
const timer = setInterval(() => {
setTimeRemaining(prev => {
if (prev <= 1) {
clearInterval(timer)
alert('Time is up!')
return 0
}
return prev - 1
})
}, 1000)
}
const submitSolution = async () => {
try {
const response = await fetch('http://127.0.0.1:5000/api/exam/submit-solution', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code })
})
const data = await response.json()
if (data.success) {
alert(`Solution submitted! Your score: ${data.score}%`)
fetchLeaderboard()
}
} catch (error) {
alert('Failed to submit solution')
}
}
const fetchLeaderboard = async () => {
try {
const response = await fetch(`http://127.0.0.1:5000/api/exam/leaderboard/${examId}`)
const data = await response.json()
setLeaderboard(data.leaderboard)
} catch (error) {
console.error('Failed to fetch leaderboard')
}
}
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
// Role Selection Screen
if (userRole === 'selector') {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-900 to-purple-900 flex items-center justify-center">
<div className="bg-white rounded-lg shadow-xl p-8 max-w-md w-full">
<h1 className="text-2xl font-bold text-center mb-8">OpenLearnX Coding Exam</h1>
<div className="space-y-4">
<button
onClick={() => setUserRole('host')}
className="w-full bg-blue-600 hover:bg-blue-700 text-white py-3 px-4 rounded-lg flex items-center justify-center space-x-2"
>
<Users className="h-5 w-5" />
<span>Host an Exam</span>
</button>
<button
onClick={() => setUserRole('participant')}
className="w-full bg-green-600 hover:bg-green-700 text-white py-3 px-4 rounded-lg flex items-center justify-center space-x-2"
>
<Play className="h-5 w-5" />
<span>Join an Exam</span>
</button>
</div>
</div>
</div>
)
}
// Host Setup Screen
if (userRole === 'host' && !examId) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-900 to-purple-900 flex items-center justify-center">
<div className="bg-white rounded-lg shadow-xl p-8 max-w-md w-full">
<h1 className="text-2xl font-bold text-center mb-8">Host Coding Exam</h1>
<div className="space-y-4">
<input
type="text"
placeholder="Enter your name"
value={participantName}
onChange={(e) => setParticipantName(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-lg"
/>
<button
onClick={createExam}
disabled={!participantName}
className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white py-3 px-4 rounded-lg"
>
Create Exam
</button>
</div>
</div>
</div>
)
}
// Join Exam Screen
if (userRole === 'participant' && !examInfo) {
return (
<div className="min-h-screen bg-gradient-to-br from-green-900 to-blue-900 flex items-center justify-center">
<div className="bg-white rounded-lg shadow-xl p-8 max-w-md w-full">
<h1 className="text-2xl font-bold text-center mb-8">Join Coding Exam</h1>
<div className="space-y-4">
<input
type="text"
placeholder="Enter exam code"
value={examId}
onChange={(e) => setExamId(e.target.value.toUpperCase())}
className="w-full p-3 border border-gray-300 rounded-lg"
/>
<input
type="text"
placeholder="Enter your name"
value={participantName}
onChange={(e) => setParticipantName(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-lg"
/>
<button
onClick={joinExam}
disabled={!examId || !participantName}
className="w-full bg-green-600 hover:bg-green-700 disabled:bg-gray-400 text-white py-3 px-4 rounded-lg"
>
Join Exam
</button>
</div>
</div>
</div>
)
}
// System Requirements Check
if (!systemChecked) {
return (
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
<div className="bg-gray-800 rounded-lg p-8 max-w-lg w-full">
<h1 className="text-2xl font-bold mb-6">System Requirements Check</h1>
<div className="space-y-4 mb-6">
<div className="flex items-center space-x-2">
<Shield className="h-5 w-5 text-green-400" />
<span>Fullscreen mode support</span>
</div>
<div className="flex items-center space-x-2">
<Lock className="h-5 w-5 text-yellow-400" />
<span>Copy/paste will be disabled</span>
</div>
<div className="flex items-center space-x-2">
<AlertTriangle className="h-5 w-5 text-red-400" />
<span>Virtual environments will be detected</span>
</div>
</div>
<button
onClick={acceptSystemRequirements}
className="w-full bg-red-600 hover:bg-red-700 text-white py-3 px-4 rounded-lg"
>
Accept & Enter Secure Mode
</button>
</div>
</div>
)
}
// Main Exam Interface
return (
<div className="min-h-screen bg-gray-900 text-white">
{/* Security Status Bar */}
<div className="bg-red-900 text-white p-2 flex items-center justify-between">
<div className="flex items-center space-x-4">
<Shield className="h-4 w-4" />
<span className="text-sm font-medium">SECURE MODE ACTIVE</span>
<Lock className="h-4 w-4" />
<span className="text-sm">Copy/Paste Disabled</span>
</div>
<div className="flex items-center space-x-4">
{timeRemaining > 0 && (
<div className="flex items-center space-x-2">
<Clock className="h-4 w-4" />
<span className="font-mono">{formatTime(timeRemaining)}</span>
</div>
)}
<span>Exam: {examId}</span>
</div>
</div>
<div className="flex h-screen">
{/* Main Coding Area */}
<div className="flex-1 p-6">
{/* Problem Description */}
<div className="bg-gray-800 rounded-lg p-6 mb-6">
<h2 className="text-xl font-bold mb-4">Problem: String Capitalizer</h2>
<p className="mb-4">Write a function that converts a string to uppercase.</p>
<div className="bg-gray-900 p-4 rounded">
<code>
{`def capitalize_string(text):
# Your code here
pass
# Test: capitalize_string("hello") should return "HELLO"`}
</code>
</div>
</div>
{/* Code Editor */}
<div className="bg-gray-800 rounded-lg p-4">
<h3 className="text-lg font-bold mb-4">Code Editor</h3>
<textarea
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="def capitalize_string(text):\n # Your code here\n pass"
className="w-full h-64 bg-gray-900 text-green-400 font-mono p-4 rounded border border-gray-600 resize-none"
style={{ userSelect: 'none', WebkitUserSelect: 'none' }}
/>
<div className="flex space-x-4 mt-4">
<button
onClick={submitSolution}
className="bg-green-600 hover:bg-green-700 px-6 py-2 rounded flex items-center space-x-2"
>
<Play className="h-4 w-4" />
<span>Submit Solution</span>
</button>
{userRole === 'host' && examInfo?.status === 'waiting' && (
<button
onClick={startExam}
className="bg-blue-600 hover:bg-blue-700 px-6 py-2 rounded"
>
Start Exam
</button>
)}
</div>
</div>
</div>
{/* Leaderboard Sidebar */}
<div className="w-80 bg-gray-800 p-6">
<div className="flex items-center space-x-2 mb-4">
<Trophy className="h-6 w-6 text-yellow-400" />
<h3 className="text-xl font-bold">Leaderboard</h3>
</div>
<div className="space-y-2">
{leaderboard.map((participant, index) => (
<div key={index} className={`p-3 rounded ${index === 0 ? 'bg-yellow-900' : index === 1 ? 'bg-gray-700' : index === 2 ? 'bg-orange-900' : 'bg-gray-700'}`}>
<div className="flex justify-between items-center">
<span className="font-medium">
{index + 1}. {participant.name}
</span>
<span className="font-bold">{participant.score}%</span>
</div>
</div>
))}
</div>
<button
onClick={fetchLeaderboard}
className="w-full mt-4 bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded text-sm"
>
Refresh Leaderboard
</button>
</div>
</div>
</div>
)
}
+416
View File
@@ -0,0 +1,416 @@
'use client'
import React, { useState, useEffect } from 'react'
import { Play, Square, Download, Upload, Settings, Clock, MemoryStick, Cpu } from 'lucide-react'
interface ExecutionResult {
success: boolean
execution_id: string
output: string
error: string
execution_time: number
memory_used: number
exit_code: number
language: string
timestamp: string
}
interface Language {
id: string
name: string
extension: string
timeout: number
memory_limit: string
}
export default function RealCompilerInterface() {
const [code, setCode] = useState('')
const [input, setInput] = useState('')
const [output, setOutput] = useState('')
const [selectedLanguage, setSelectedLanguage] = useState('python')
const [languages, setLanguages] = useState<Language[]>([])
const [isExecuting, setIsExecuting] = useState(false)
const [executionResult, setExecutionResult] = useState<ExecutionResult | null>(null)
const [executionHistory, setExecutionHistory] = useState<ExecutionResult[]>([])
const languageTemplates: { [key: string]: string } = {
python: `# Python Code
print("Hello World!")
name = input("Enter your name: ")
print(f"Hello, {name}!")`,
java: `public class Main {
public static void main(String[] args) {
System.out.println("Hello World!");
// Your code here
}
}`,
cpp: `#include <iostream>
#include <string>
using namespace std;
int main() {
cout << "Hello World!" << endl;
string name;
cout << "Enter your name: ";
getline(cin, name);
cout << "Hello, " << name << "!" << endl;
return 0;
}`,
c: `#include <stdio.h>
int main() {
printf("Hello World!\\n");
char name[100];
printf("Enter your name: ");
fgets(name, sizeof(name), stdin);
printf("Hello, %s", name);
return 0;
}`,
javascript: `// JavaScript Code
console.log("Hello World!");
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question('Enter your name: ', (name) => {
console.log(\`Hello, \${name}!\`);
rl.close();
});`,
go: `package main
import (
"fmt"
"bufio"
"os"
)
func main() {
fmt.Println("Hello World!")
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter your name: ")
name, _ := reader.ReadString('\\n')
fmt.Printf("Hello, %s", name)
}`,
rust: `use std::io;
fn main() {
println!("Hello World!");
println!("Enter your name: ");
let mut name = String::new();
io::stdin().read_line(&mut name).expect("Failed to read line");
println!("Hello, {}!", name.trim());
}`
}
useEffect(() => {
fetchSupportedLanguages()
}, [])
useEffect(() => {
if (selectedLanguage && languageTemplates[selectedLanguage] && !code) {
setCode(languageTemplates[selectedLanguage])
}
}, [selectedLanguage])
const fetchSupportedLanguages = async () => {
try {
const response = await fetch('http://127.0.0.1:5000/api/compiler/languages')
const data = await response.json()
if (data.success) {
setLanguages(data.languages)
}
} catch (error) {
console.error('Failed to fetch languages:', error)
}
}
const executeCode = async () => {
if (!code.trim()) {
alert('Please write some code first!')
return
}
setIsExecuting(true)
setOutput('')
setExecutionResult(null)
try {
const response = await fetch('http://127.0.0.1:5000/api/compiler/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code,
language: selectedLanguage,
input: input
})
})
const result = await response.json()
if (result.success) {
setOutput(result.output || result.error || 'No output')
setExecutionResult(result)
// Add to history
setExecutionHistory(prev => [result, ...prev.slice(0, 9)]) // Keep last 10
} else {
setOutput(`Error: ${result.error}`)
}
} catch (error) {
setOutput(`Execution failed: ${(error as Error).message}`)
} finally {
setIsExecuting(false)
}
}
const testCompiler = async () => {
try {
const response = await fetch('http://127.0.0.1:5000/api/compiler/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ language: selectedLanguage })
})
const result = await response.json()
if (result.success) {
setOutput(result.output)
alert('Compiler test successful!')
} else {
setOutput(`Test failed: ${result.error}`)
}
} catch (error) {
setOutput(`Test failed: ${(error as Error).message}`)
}
}
const downloadCode = () => {
const language = languages.find(l => l.id === selectedLanguage)
const extension = language?.extension || '.txt'
const blob = new Blob([code], { type: 'text/plain' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `code${extension}`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
const loadCodeFile = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (file) {
const reader = new FileReader()
reader.onload = (e) => {
const content = e.target?.result as string
setCode(content)
}
reader.readAsText(file)
}
}
const clearAll = () => {
setCode('')
setInput('')
setOutput('')
setExecutionResult(null)
}
return (
<div className="min-h-screen bg-gray-900 text-white">
{/* Header */}
<div className="bg-gray-800 border-b border-gray-700 p-4">
<div className="max-w-7xl mx-auto">
<div className="flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold">OpenLearnX Real Compiler</h1>
<p className="text-gray-400">Execute code in multiple programming languages with real output</p>
</div>
<div className="flex items-center space-x-4">
<button
onClick={testCompiler}
className="bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded flex items-center space-x-2"
>
<Settings className="h-4 w-4" />
<span>Test Compiler</span>
</button>
<div className="text-sm text-gray-400">
{languages.length} languages supported
</div>
</div>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto p-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Code Editor */}
<div className="space-y-4">
{/* Language Selector & Controls */}
<div className="bg-gray-800 rounded-lg p-4">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-bold">Code Editor</h2>
<div className="flex items-center space-x-2">
<select
value={selectedLanguage}
onChange={(e) => setSelectedLanguage(e.target.value)}
className="bg-gray-700 text-white px-3 py-1 rounded border border-gray-600"
>
{languages.map(lang => (
<option key={lang.id} value={lang.id}>
{lang.name} ({lang.extension})
</option>
))}
</select>
<input
type="file"
accept=".py,.java,.cpp,.c,.js,.go,.rs,.sh"
onChange={loadCodeFile}
className="hidden"
id="file-upload"
/>
<label
htmlFor="file-upload"
className="bg-gray-600 hover:bg-gray-700 px-3 py-1 rounded cursor-pointer"
>
<Upload className="h-4 w-4" />
</label>
<button
onClick={downloadCode}
className="bg-gray-600 hover:bg-gray-700 px-3 py-1 rounded"
>
<Download className="h-4 w-4" />
</button>
</div>
</div>
<textarea
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="Write your code here..."
className="w-full h-80 bg-gray-900 text-green-400 font-mono p-4 rounded border border-gray-600 resize-none focus:ring-2 focus:ring-blue-500"
spellCheck={false}
/>
<div className="flex items-center justify-between mt-4">
<div className="text-sm text-gray-400">
Language: {languages.find(l => l.id === selectedLanguage)?.name}
{executionResult && (
<span className="ml-4">
Last execution: {executionResult.execution_time}s
</span>
)}
</div>
<div className="flex space-x-2">
<button
onClick={clearAll}
className="bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded text-sm"
>
Clear All
</button>
<button
onClick={executeCode}
disabled={isExecuting}
className="bg-green-600 hover:bg-green-700 disabled:bg-gray-600 px-6 py-2 rounded flex items-center space-x-2"
>
<Play className="h-4 w-4" />
<span>{isExecuting ? 'Executing...' : 'Run Code'}</span>
</button>
</div>
</div>
</div>
{/* Input Section */}
<div className="bg-gray-800 rounded-lg p-4">
<h3 className="text-lg font-bold mb-2">Input Data</h3>
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Enter input data for your program (if needed)..."
className="w-full h-24 bg-gray-900 text-white font-mono p-3 rounded border border-gray-600 resize-none"
/>
</div>
</div>
{/* Output & Results */}
<div className="space-y-4">
{/* Output */}
<div className="bg-gray-800 rounded-lg p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-bold">Output</h3>
{executionResult && (
<div className="flex items-center space-x-4 text-sm text-gray-400">
<div className="flex items-center space-x-1">
<Clock className="h-4 w-4" />
<span>{executionResult.execution_time}s</span>
</div>
<div className="flex items-center space-x-1">
<MemoryStick className="h-4 w-4" />
<span>{Math.round(executionResult.memory_used / 1024)}KB</span>
</div>
<div className={`px-2 py-1 rounded text-xs ${
executionResult.exit_code === 0 ? 'bg-green-900 text-green-200' : 'bg-red-900 text-red-200'
}`}>
Exit: {executionResult.exit_code}
</div>
</div>
)}
</div>
<div className="bg-black p-4 rounded h-80 overflow-y-auto">
<pre className="text-green-400 font-mono whitespace-pre-wrap text-sm">
{output || 'No output yet. Run your code to see results here.'}
</pre>
</div>
</div>
{/* Execution History */}
{executionHistory.length > 0 && (
<div className="bg-gray-800 rounded-lg p-4">
<h3 className="text-lg font-bold mb-4">Execution History</h3>
<div className="space-y-2 max-h-40 overflow-y-auto">
{executionHistory.map((result, index) => (
<div
key={result.execution_id}
className="flex items-center justify-between p-2 bg-gray-700 rounded text-sm cursor-pointer hover:bg-gray-600"
onClick={() => {
setOutput(result.output || result.error)
setExecutionResult(result)
}}
>
<div>
<span className="font-medium">{result.language}</span>
<span className="text-gray-400 ml-2">
{new Date(result.timestamp).toLocaleTimeString()}
</span>
</div>
<div className="flex items-center space-x-2">
<span>{result.execution_time}s</span>
<div className={`w-2 h-2 rounded-full ${
result.exit_code === 0 ? 'bg-green-400' : 'bg-red-400'
}`}></div>
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
</div>
)
}
+99
View File
@@ -0,0 +1,99 @@
'use client'
import { useState } from 'react'
export default function JoinTestPage() {
const [examCode, setExamCode] = useState('')
const [studentName, setStudentName] = useState('')
const [loading, setLoading] = useState(false)
const handleJoin = async () => {
if (!examCode || !studentName) {
alert('Fill both fields')
return
}
setLoading(true)
try {
const payload = {
exam_code: examCode.trim(), // CORRECT field name
student_name: studentName.trim() // CORRECT field name
}
console.log('🚀 Sending:', payload)
const response = await fetch('http://127.0.0.1:5000/api/exam/join-exam', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
const data = await response.json()
console.log('📦 Response:', data)
if (data.success) {
alert('✅ SUCCESS: ' + data.exam_info.title)
} else {
alert('❌ ERROR: ' + data.error)
}
} catch (error) {
alert('❌ Network error')
} finally {
setLoading(false)
}
}
return (
<div style={{ padding: '50px', background: '#000', color: 'white', minHeight: '100vh' }}>
<h1>🧪 TEST JOIN PAGE - BYPASS CACHE</h1>
<div style={{ maxWidth: '400px', marginTop: '30px' }}>
<input
value={examCode}
onChange={e => setExamCode(e.target.value)}
placeholder="6884F82A7300F2AD9CFC974A"
style={{
width: '100%',
padding: '10px',
margin: '10px 0',
background: '#333',
color: 'white',
fontFamily: 'monospace'
}}
/>
<input
value={studentName}
onChange={e => setStudentName(e.target.value)}
placeholder="Your name"
style={{
width: '100%',
padding: '10px',
margin: '10px 0',
background: '#333',
color: 'white'
}}
/>
<button
onClick={handleJoin}
disabled={loading}
style={{
width: '100%',
padding: '15px',
background: loading ? '#666' : '#00ff00',
color: 'black',
border: 'none',
fontWeight: 'bold'
}}
>
{loading ? 'JOINING...' : 'TEST JOIN'}
</button>
</div>
<div style={{ marginTop: '20px', background: '#333', padding: '10px' }}>
<p>Will send: exam_code="{examCode}" student_name="{studentName}"</p>
</div>
</div>
)
}
+50
View File
@@ -0,0 +1,50 @@
// Additional client-side security measures
(function() {
'use strict';
// Detect common coding extensions
const suspiciousExtensions = [
'bfnaelmomeimhlpmgjnjophhpkkoljpa', // Honey
'cjpalhdlnbpafiamejdnhcphjbkeiagm', // uBlock Origin
'gighmmpiobklfepjocnamgkkbiglidom', // AdBlock
'hdokiejnpimakedhajhdlcegeplioahd', // LastPass
'fhbjgbiflinjbdggehcddcbncdddomop', // Postman
'hgmloofddffdnphfgcellkdfbfbjeloo' // TablePlus
];
// Check for extension APIs
if (typeof chrome !== 'undefined' && chrome.runtime) {
suspiciousExtensions.forEach(extensionId => {
chrome.runtime.sendMessage(extensionId, {ping: true}, (response) => {
if (response) {
alert('Coding extensions detected. Please disable all extensions.');
window.location.href = '/';
}
});
});
}
// Monitor for suspicious DOM modifications
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) { // Element node
const classes = node.className || '';
if (classes.includes('extension-') ||
classes.includes('chrome-extension-') ||
node.tagName === 'IFRAME' && node.src.includes('extension://')) {
alert('Extension interference detected');
window.location.href = '/';
}
}
});
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
})();
+332
View File
@@ -0,0 +1,332 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenLearnX - Test Join Exam</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.container {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 40px;
max-width: 500px;
width: 90%;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
font-size: 28px;
margin-bottom: 10px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
}
.form-group input {
width: 100%;
padding: 15px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 10px;
background: rgba(255, 255, 255, 0.1);
color: white;
font-size: 16px;
transition: all 0.3s ease;
}
.form-group input::placeholder {
color: rgba(255, 255, 255, 0.6);
}
.form-group input:focus {
outline: none;
border-color: #4CAF50;
background: rgba(255, 255, 255, 0.15);
}
.exam-code-input {
font-family: 'Courier New', monospace;
font-size: 14px;
}
.join-btn {
width: 100%;
padding: 18px;
background: linear-gradient(45deg, #4CAF50, #45a049);
color: white;
border: none;
border-radius: 10px;
font-size: 18px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 10px;
}
.join-btn:hover:not(:disabled) {
background: linear-gradient(45deg, #45a049, #3d8b40);
transform: translateY(-2px);
}
.join-btn:disabled {
background: rgba(255, 255, 255, 0.3);
cursor: not-allowed;
}
.error {
background: linear-gradient(45deg, #f44336, #d32f2f);
padding: 15px;
margin: 15px 0;
border-radius: 10px;
display: none;
}
.success {
background: linear-gradient(45deg, #4CAF50, #45a049);
padding: 15px;
margin: 15px 0;
border-radius: 10px;
display: none;
}
.debug {
background: rgba(0, 0, 0, 0.3);
padding: 15px;
margin: 20px 0;
border-radius: 10px;
font-size: 12px;
font-family: 'Courier New', monospace;
word-break: break-all;
}
.example-code {
background: rgba(76, 175, 80, 0.2);
padding: 8px 12px;
border-radius: 5px;
font-family: 'Courier New', monospace;
font-weight: bold;
color: #4CAF50;
margin-top: 5px;
font-size: 12px;
word-break: break-all;
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s linear infinite;
margin-right: 10px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🚀 Join Coding Exam</h1>
<p>Test page for OpenLearnX exam joining</p>
</div>
<form id="joinForm">
<div class="form-group">
<label for="examCode">📝 Exam Code:</label>
<input
type="text"
id="examCode"
class="exam-code-input"
placeholder="Enter exam code (6 chars or MongoDB ID)"
autocomplete="off"
required
>
<div class="example-code">Try: 6884f04c6ca73cc9032deaf9</div>
</div>
<div class="form-group">
<label for="studentName">👤 Your Name:</label>
<input
type="text"
id="studentName"
placeholder="Enter your full name"
autocomplete="name"
required
>
</div>
<button type="submit" id="joinBtn" class="join-btn">
<span id="btnText">Join Exam</span>
</button>
</form>
<div id="error" class="error"></div>
<div id="success" class="success"></div>
<div id="debug" class="debug">
<h4>🔧 Debug Info:</h4>
<p>Exam Code: "<span id="debugCode"></span>"</p>
<p>Student Name: "<span id="debugName"></span>"</p>
<p>Status: <span id="debugStatus">Ready to join</span></p>
</div>
</div>
<script>
const examCodeInput = document.getElementById('examCode');
const studentNameInput = document.getElementById('studentName');
const joinBtn = document.getElementById('joinBtn');
const btnText = document.getElementById('btnText');
const errorDiv = document.getElementById('error');
const successDiv = document.getElementById('success');
const debugCode = document.getElementById('debugCode');
const debugName = document.getElementById('debugName');
const debugStatus = document.getElementById('debugStatus');
const API_BASE = 'http://127.0.0.1:5000';
function updateDebug() {
debugCode.textContent = examCodeInput.value;
debugName.textContent = studentNameInput.value;
}
function showError(message) {
errorDiv.textContent = '❌ ' + message;
errorDiv.style.display = 'block';
successDiv.style.display = 'none';
debugStatus.textContent = 'Error: ' + message;
}
function showSuccess(message) {
successDiv.textContent = '✅ ' + message;
successDiv.style.display = 'block';
errorDiv.style.display = 'none';
debugStatus.textContent = 'Success: ' + message;
}
function setLoading(loading) {
if (loading) {
joinBtn.disabled = true;
btnText.innerHTML = '<span class="loading"></span>Joining Exam...';
} else {
joinBtn.disabled = false;
btnText.textContent = 'Join Exam';
}
}
examCodeInput.addEventListener('input', updateDebug);
studentNameInput.addEventListener('input', updateDebug);
document.getElementById('joinForm').addEventListener('submit', async (e) => {
e.preventDefault();
const examCode = examCodeInput.value.trim();
const studentName = studentNameInput.value.trim();
console.log('🚀 Form submitted with:', { examCode, studentName });
if (!examCode || !studentName) {
showError('Please fill in both fields');
return;
}
setLoading(true);
try {
const payload = {
exam_code: examCode,
student_name: studentName
};
console.log('📤 Sending payload:', payload);
const response = await fetch(`${API_BASE}/api/exam/join-exam`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(payload)
});
console.log('📡 Response status:', response.status);
const data = await response.json();
console.log('📦 Response data:', data);
if (data.success) {
showSuccess(`Successfully joined: ${data.exam_info.title}`);
localStorage.setItem('exam_session', JSON.stringify({
exam_code: examCode,
student_name: studentName,
exam_info: data.exam_info,
joined_at: new Date().toISOString()
}));
setTimeout(() => {
if (confirm('✅ Successfully joined exam!\n\nRedirect to exam interface?')) {
window.location.href = '/coding/exam';
}
}, 1000);
} else {
showError(data.error || 'Failed to join exam');
}
} catch (error) {
console.error('❌ Network error:', error);
showError('Network error: Cannot connect to backend');
} finally {
setLoading(false);
}
});
// Auto-fill for testing
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 't') {
e.preventDefault();
examCodeInput.value = '6884f04c6ca73cc9032deaf9';
studentNameInput.value = 'Test Student';
updateDebug();
console.log('🧪 Test data filled');
}
});
updateDebug();
</script>
</body>
</html>
+3
View File
@@ -31,3 +31,6 @@ cryptography==41.0.7
# HTTP requests
requests==2.31.0
#docker
docker