qizz + panel

This commit is contained in:
5t4l1n
2025-07-28 00:15:37 +05:30
parent cbecb72cc9
commit efd6708e5a
43 changed files with 61721 additions and 6158 deletions
-690
View File
@@ -1,690 +0,0 @@
{
"contract_address": "0xC2FE2F49B3a1384aEdFAae127F054FAf216eF684",
"transaction_hash": "0xfe5a433dae316bd2d60b7190c21866a1fde30777f08d9d37e403ed642433fa28",
"deployer": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"network": "local",
"abi": [
{
"type": "constructor",
"inputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "approve",
"inputs": [
{
"name": "to",
"type": "address",
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "balanceOf",
"inputs": [
{
"name": "owner",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "certificates",
"inputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "subject",
"type": "string",
"internalType": "string"
},
{
"name": "studentName",
"type": "string",
"internalType": "string"
},
{
"name": "score",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "timestamp",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "verified",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "getApproved",
"inputs": [
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "getCertificate",
"inputs": [
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "tuple",
"internalType": "struct CertificateNFT.Certificate",
"components": [
{
"name": "subject",
"type": "string",
"internalType": "string"
},
{
"name": "studentName",
"type": "string",
"internalType": "string"
},
{
"name": "score",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "timestamp",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "verified",
"type": "bool",
"internalType": "bool"
}
]
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "getUserCertificates",
"inputs": [
{
"name": "user",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "",
"type": "uint256[]",
"internalType": "uint256[]"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "isApprovedForAll",
"inputs": [
{
"name": "owner",
"type": "address",
"internalType": "address"
},
{
"name": "operator",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "mintCertificate",
"inputs": [
{
"name": "to",
"type": "address",
"internalType": "address"
},
{
"name": "_tokenURI",
"type": "string",
"internalType": "string"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "mintCertificateWithDetails",
"inputs": [
{
"name": "to",
"type": "address",
"internalType": "address"
},
{
"name": "_tokenURI",
"type": "string",
"internalType": "string"
},
{
"name": "subject",
"type": "string",
"internalType": "string"
},
{
"name": "studentName",
"type": "string",
"internalType": "string"
},
{
"name": "score",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "name",
"inputs": [],
"outputs": [
{
"name": "",
"type": "string",
"internalType": "string"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "owner",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "ownerOf",
"inputs": [
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "renounceOwnership",
"inputs": [],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "safeTransferFrom",
"inputs": [
{
"name": "from",
"type": "address",
"internalType": "address"
},
{
"name": "to",
"type": "address",
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "safeTransferFrom",
"inputs": [
{
"name": "from",
"type": "address",
"internalType": "address"
},
{
"name": "to",
"type": "address",
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "data",
"type": "bytes",
"internalType": "bytes"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "setApprovalForAll",
"inputs": [
{
"name": "operator",
"type": "address",
"internalType": "address"
},
{
"name": "approved",
"type": "bool",
"internalType": "bool"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "supportsInterface",
"inputs": [
{
"name": "interfaceId",
"type": "bytes4",
"internalType": "bytes4"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "symbol",
"inputs": [],
"outputs": [
{
"name": "",
"type": "string",
"internalType": "string"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "tokenURI",
"inputs": [
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "string",
"internalType": "string"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "totalSupply",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "transferFrom",
"inputs": [
{
"name": "from",
"type": "address",
"internalType": "address"
},
{
"name": "to",
"type": "address",
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "transferOwnership",
"inputs": [
{
"name": "newOwner",
"type": "address",
"internalType": "address"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "userCertificates",
"inputs": [
{
"name": "",
"type": "address",
"internalType": "address"
},
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "verifyCertificate",
"inputs": [
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"type": "event",
"name": "Approval",
"inputs": [
{
"name": "owner",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "approved",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"indexed": true,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"type": "event",
"name": "ApprovalForAll",
"inputs": [
{
"name": "owner",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "operator",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "approved",
"type": "bool",
"indexed": false,
"internalType": "bool"
}
],
"anonymous": false
},
{
"type": "event",
"name": "BatchMetadataUpdate",
"inputs": [
{
"name": "_fromTokenId",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "_toTokenId",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"type": "event",
"name": "CertificateMinted",
"inputs": [
{
"name": "tokenId",
"type": "uint256",
"indexed": true,
"internalType": "uint256"
},
{
"name": "student",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "subject",
"type": "string",
"indexed": false,
"internalType": "string"
},
{
"name": "score",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "tokenURI",
"type": "string",
"indexed": false,
"internalType": "string"
}
],
"anonymous": false
},
{
"type": "event",
"name": "MetadataUpdate",
"inputs": [
{
"name": "_tokenId",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"type": "event",
"name": "OwnershipTransferred",
"inputs": [
{
"name": "previousOwner",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "newOwner",
"type": "address",
"indexed": true,
"internalType": "address"
}
],
"anonymous": false
},
{
"type": "event",
"name": "Transfer",
"inputs": [
{
"name": "from",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "to",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"indexed": true,
"internalType": "uint256"
}
],
"anonymous": false
}
],
"gas_used": 3387337,
"block_number": 22994809,
"status": 1
}
-11
View File
@@ -1,11 +0,0 @@
[profile.default]
src = "contracts"
out = "out"
libs = ["lib"]
remappings = [
"@openzeppelin/=lib/openzeppelin-contracts/"
]
[rpc_endpoints]
local = "http://127.0.0.1:8545"
sepolia = "https://sepolia.infura.io/v3/${INFURA_API_KEY}"
File diff suppressed because it is too large Load Diff
-30
View File
@@ -1,30 +0,0 @@
from bson import ObjectId
from datetime import datetime
from pymongo.collection import Collection
class UserModel:
def __init__(self, collection: Collection):
self.collection = collection
async def get_by_wallet(self, wallet_address: str):
return await self.collection.find_one({"wallet_address": wallet_address.lower()})
async def create_user(self, wallet_address: str):
now = datetime.utcnow()
user = {
"wallet_address": wallet_address.lower(),
"created_at": now,
"last_login": now,
"total_tests": 0,
"certificates": []
}
result = await self.collection.insert_one(user)
user["_id"] = result.inserted_id
return user
async def update_last_login(self, wallet_address: str):
now = datetime.utcnow()
await self.collection.update_one(
{"wallet_address": wallet_address.lower()},
{"$set": {"last_login": now}}
)
-142
View File
@@ -1,142 +0,0 @@
from motor.motor_asyncio import AsyncIOMotorClient
from pymongo.errors import ServerSelectionTimeoutError
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Any
class MongoService:
def __init__(self, uri: str):
self.uri = uri # Store URI for sync operations
try:
# Simple connection without custom SSL context
self.client = AsyncIOMotorClient(
uri,
serverSelectionTimeoutMS=30000,
connectTimeoutMS=30000,
socketTimeoutMS=30000
)
print("MongoDB client initialized successfully")
except Exception as e:
print(f"MongoDB connection failed: {e}")
# Fallback to basic connection
self.client = AsyncIOMotorClient(uri)
self.db = self.client.openlearnx
# Collections
self.users = self.db.users
self.questions = self.db.questions
self.test_sessions = self.db.test_sessions
self.certificates = self.db.certificates
self.peer_reviews = self.db.peer_reviews
async def init_db(self):
"""Initialize database with indexes and sample data"""
try:
# Test connection first
await self.client.admin.command('ping')
print("MongoDB connection successful!")
# Create indexes
await self.users.create_index("wallet_address", unique=True)
await self.users.create_index("email", unique=True, sparse=True)
await self.questions.create_index("subject")
await self.questions.create_index("difficulty")
await self.test_sessions.create_index("user_id")
await self.test_sessions.create_index("created_at")
await self.certificates.create_index("user_id")
await self.certificates.create_index("token_id", unique=True)
# Insert sample questions if none exist
if await self.questions.count_documents({}) == 0:
await self.insert_sample_questions()
print("Sample questions inserted successfully")
except ServerSelectionTimeoutError as e:
print(f"Failed to connect to MongoDB: {e}")
print("Continuing without database initialization...")
except Exception as e:
print(f"Database initialization error: {e}")
print("Continuing without database initialization...")
async def get_user_by_wallet(self, wallet_address: str):
"""Get user by wallet address"""
return await self.users.find_one({"wallet_address": wallet_address.lower()})
async def create_user(self, wallet_address: str):
"""Create a new user"""
now = datetime.utcnow()
user = {
"wallet_address": wallet_address.lower(),
"created_at": now,
"last_login": now,
"total_tests": 0,
"certificates": []
}
result = await self.users.insert_one(user)
user["_id"] = result.inserted_id
return user
async def update_user_login(self, wallet_address: str):
"""Update user's last login time"""
await self.users.update_one(
{"wallet_address": wallet_address.lower()},
{"$set": {"last_login": datetime.utcnow()}}
)
async def insert_sample_questions(self):
"""Insert sample questions - implement based on your needs"""
# You'll need to implement this method based on your question structure
sample_questions = [
{
"subject": "Python",
"difficulty": "beginner",
"question": "What is a variable in Python?",
"options": ["A storage location", "A function", "A loop", "A condition"],
"correct_answer": 0,
"created_at": datetime.utcnow()
},
# Add more sample questions as needed
]
await self.questions.insert_many(sample_questions)
async def close_connection(self):
"""Close the database connection"""
if self.client:
self.client.close()
print("MongoDB connection closed")
def create_user_sync(self, wallet_address: str):
"""Synchronous user creation using pymongo instead of motor"""
import pymongo
# Create a synchronous connection for this operation only
client = pymongo.MongoClient(self.uri)
db = client.openlearnx
users = db.users
try:
# Check if user exists
user = users.find_one({"wallet_address": wallet_address.lower()})
if not user:
# Create new user
new_user = {
"wallet_address": wallet_address.lower(),
"created_at": datetime.utcnow(),
"last_login": datetime.utcnow(),
"total_tests": 0,
"certificates": []
}
result = users.insert_one(new_user)
new_user["_id"] = result.inserted_id
return new_user
else:
# Update last login
users.update_one(
{"wallet_address": wallet_address.lower()},
{"$set": {"last_login": datetime.utcnow()}}
)
return user
finally:
# Always close the connection
client.close()
-428
View File
@@ -1,428 +0,0 @@
from flask import Blueprint, request, jsonify
from functools import wraps
import uuid
from datetime import datetime
from pymongo import MongoClient
import os
from bson import ObjectId
bp = Blueprint('admin', __name__)
# MongoDB connection
mongo_uri = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/')
client = MongoClient(mongo_uri)
db = client.openlearnx
def admin_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
try:
auth_header = request.headers.get('Authorization')
print(f"Admin auth check - Header: {auth_header}")
if not auth_header:
print("❌ No Authorization header")
return jsonify({"error": "No authorization header provided"}), 401
if not auth_header.startswith('Bearer '):
print("❌ Invalid authorization format")
return jsonify({"error": "Invalid authorization format"}), 401
token = auth_header.split(' ')[1] if len(auth_header.split(' ')) > 1 else None
print(f"Extracted token: '{token}'")
# Check environment variable first, then fallback to default
expected_token = os.getenv('ADMIN_TOKEN')
if not expected_token:
expected_token = 'admin-secret-key'
print(f"Expected token: '{expected_token}'")
print(f"Environment ADMIN_TOKEN: '{os.getenv('ADMIN_TOKEN')}'")
# Strip any whitespace from both tokens
if token and expected_token:
if token.strip() == expected_token.strip():
print("✅ Admin authentication successful")
return f(*args, **kwargs)
print("❌ Token mismatch")
return jsonify({"error": "Invalid admin token"}), 401
except Exception as e:
print(f"❌ Admin auth error: {str(e)}")
return jsonify({"error": "Authentication failed"}), 500
return decorated_function
def serialize_course(course):
"""Convert MongoDB document to JSON-serializable format"""
if course:
if '_id' in course:
del course['_id']
return course
return None
def convert_to_embed_url(youtube_url):
"""Convert YouTube watch URL to embed URL - ENHANCED VERSION"""
if not youtube_url:
return None
try:
if "youtu.be/" in youtube_url:
video_id = youtube_url.split("youtu.be/")[1].split("?")[0].split("&")[0]
elif "youtube.com/watch?v=" in youtube_url:
video_id = youtube_url.split("v=")[1].split("&")[0]
elif "youtube.com/embed/" in youtube_url:
return youtube_url
else:
return None
video_id = video_id.strip()
return f"https://www.youtube.com/embed/{video_id}?rel=0&modestbranding=1"
except Exception as e:
print(f"Error converting YouTube URL: {e}")
return None
@bp.route("/test", methods=["GET"])
@admin_required
def test_admin():
"""Test admin authentication"""
return jsonify({
"success": True,
"message": "Admin authentication working",
"timestamp": datetime.now().isoformat()
})
@bp.route("/dashboard", methods=["GET"])
@admin_required
def admin_dashboard():
"""Get admin dashboard statistics"""
try:
total_courses = db.courses.count_documents({})
total_lessons = db.lessons.count_documents({})
active_students = db.users.count_documents({"status": "active"}) or 2341
stats = {
"total_courses": total_courses,
"total_lessons": total_lessons,
"active_students": active_students,
"completion_rate": 78
}
return jsonify(stats)
except Exception as e:
print(f"Dashboard error: {str(e)}")
return jsonify({"error": str(e)}), 500
@bp.route("/courses", methods=["GET"])
@admin_required
def get_admin_courses():
"""Get all courses for admin management"""
try:
print("Fetching courses from database...")
courses = list(db.courses.find({}, {"_id": 0}))
print(f"Found {len(courses)} courses")
for course in courses:
course["students"] = course.get("students", 0)
course["status"] = "published"
return jsonify(courses)
except Exception as e:
print(f"Error fetching courses: {str(e)}")
return jsonify({"error": str(e)}), 500
@bp.route("/courses", methods=["POST"])
@admin_required
def create_course():
"""Create new course"""
try:
data = request.json
print(f"Creating course with data: {data}") # Debug log
course_id = data.get('id') or f"{data.get('title', '').lower().replace(' ', '-').replace('&', 'and')}-course"
existing_course = db.courses.find_one({"id": course_id})
if existing_course:
return jsonify({"error": "Course with this ID already exists"}), 400
new_course = {
"id": course_id,
"title": data.get('title'),
"subject": data.get('subject'),
"description": data.get('description'),
"difficulty": data.get('difficulty'),
"mentor": data.get('mentor', '5t4l1n'),
"video_url": data.get('video_url'),
"embed_url": convert_to_embed_url(data.get('video_url')) if data.get('video_url') else None,
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
"students": 0,
"progress": 0,
"modules": []
}
result = db.courses.insert_one(new_course)
print(f"Course created with ID: {result.inserted_id}")
# Remove _id field before returning
new_course_response = serialize_course(new_course)
return jsonify({"success": True, "course": new_course_response}), 201
except Exception as e:
print(f"Error creating course: {e}")
return jsonify({"error": str(e)}), 500
@bp.route("/courses/<course_id>", methods=["PUT"])
@admin_required
def update_course(course_id):
"""Update existing course - FIXED VERSION"""
try:
data = request.json
print(f"Updating course {course_id} with data: {data}") # Debug log
update_data = {
"title": data.get('title'),
"subject": data.get('subject'),
"description": data.get('description'),
"difficulty": data.get('difficulty'),
"mentor": data.get('mentor'),
"video_url": data.get('video_url'),
"embed_url": convert_to_embed_url(data.get('video_url')) if data.get('video_url') else None,
"updated_at": datetime.now().isoformat()
}
# Remove None values
update_data = {k: v for k, v in update_data.items() if v is not None}
print(f"Filtered update data: {update_data}") # Debug log
result = db.courses.update_one(
{"id": course_id},
{"$set": update_data}
)
print(f"Update result: matched={result.matched_count}, modified={result.modified_count}") # Debug log
if result.matched_count == 0:
return jsonify({"error": "Course not found"}), 404
# Get updated course without _id field
updated_course = db.courses.find_one({"id": course_id}, {"_id": 0})
return jsonify({"success": True, "course": updated_course})
except Exception as e:
print(f"Error updating course: {e}")
return jsonify({"error": str(e)}), 500
@bp.route("/courses/<course_id>", methods=["DELETE"])
@admin_required
def delete_course(course_id):
"""Delete course"""
try:
print(f"Deleting course: {course_id}") # Debug log
result = db.courses.delete_one({"id": course_id})
if result.deleted_count == 0:
return jsonify({"error": "Course not found"}), 404
# Also delete related lessons
lesson_result = db.lessons.delete_many({"course_id": course_id})
print(f"Deleted {lesson_result.deleted_count} related lessons") # Debug log
return jsonify({"success": True, "message": "Course deleted successfully"})
except Exception as e:
print(f"Error deleting course: {e}")
return jsonify({"error": str(e)}), 500
@bp.route("/courses/<course_id>/modules", methods=["POST"])
@admin_required
def add_module(course_id):
"""Add module to course"""
try:
data = request.json
module = {
"id": data.get('id') or str(uuid.uuid4()),
"title": data.get('title'),
"lessons": []
}
result = db.courses.update_one(
{"id": course_id},
{"$push": {"modules": module}}
)
if result.matched_count == 0:
return jsonify({"error": "Course not found"}), 404
return jsonify({"success": True, "module": module})
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/courses/<course_id>/lessons", methods=["POST"])
@admin_required
def add_lesson(course_id):
"""Add lesson to course"""
try:
data = request.json
lesson = {
"id": data.get('id') or str(uuid.uuid4()),
"course_id": course_id,
"title": data.get('title'),
"type": data.get('type', 'video'),
"duration": data.get('duration'),
"description": data.get('description'),
"content": data.get('content'),
"video_url": data.get('video_url'),
"embed_url": convert_to_embed_url(data.get('video_url')) if data.get('video_url') else None,
"created_at": datetime.now().isoformat()
}
# Insert lesson
db.lessons.insert_one(lesson)
# Remove _id field before returning
lesson_response = serialize_course(lesson)
return jsonify({"success": True, "lesson": lesson_response})
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/initialize", methods=["POST"])
@admin_required
def initialize_default_courses():
"""Initialize database with default courses"""
try:
existing_count = db.courses.count_documents({})
if existing_count > 0:
return jsonify({"message": f"Courses already initialized ({existing_count} courses found)"}), 200
default_courses = [
{
"id": "python-course",
"title": "Python Programming Mastery",
"subject": "Programming",
"description": "Learn Python from basics to advanced concepts including turtle graphics",
"difficulty": "Beginner to Advanced",
"mentor": "5t4l1n",
"video_url": "https://youtu.be/SsH8GJlqUIg?si=cK7KW_sM0uf95lEp",
"embed_url": "https://www.youtube.com/embed/SsH8GJlqUIg?rel=0&modestbranding=1",
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
"students": 1250,
"progress": 0,
"modules": []
},
{
"id": "java-course",
"title": "Java Development Bootcamp",
"subject": "Programming",
"description": "Master Java programming with object-oriented concepts",
"difficulty": "Intermediate",
"mentor": "5t4l1n",
"video_url": "https://youtu.be/SsH8GJlqUIg?si=cK7KW_sM0uf95lEp",
"embed_url": "https://www.youtube.com/embed/SsH8GJlqUIg?rel=0&modestbranding=1",
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
"students": 890,
"progress": 0,
"modules": []
},
{
"id": "ethical-hacking-course",
"title": "Ethical Hacking & Cybersecurity",
"subject": "Cybersecurity",
"description": "Learn ethical hacking techniques and penetration testing",
"difficulty": "Advanced",
"mentor": "5t4l1n",
"video_url": "https://youtu.be/cDnX0vyNTaE?si=ZXNI4hv2HlWN7eCS",
"embed_url": "https://www.youtube.com/embed/cDnX0vyNTaE?rel=0&modestbranding=1",
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
"students": 567,
"progress": 0,
"modules": []
},
{
"id": "dark-web-hosting-course",
"title": "Learn Dark Web Hosting",
"subject": "Cybersecurity",
"description": "Understanding dark web infrastructure, Tor networks, and secure hosting practices for cybersecurity professionals",
"difficulty": "Expert",
"mentor": "5t4l1n",
"video_url": "https://youtu.be/Z4_USAMVhYs?si=Y_ThVisph5ekM44U",
"embed_url": "https://www.youtube.com/embed/Z4_USAMVhYs?rel=0&modestbranding=1",
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
"students": 234,
"progress": 0,
"modules": []
}
]
result = db.courses.insert_many(default_courses)
print(f"Initialized {len(result.inserted_ids)} default courses")
return jsonify({
"success": True,
"message": f"Default courses initialized successfully",
"courses_created": len(result.inserted_ids)
})
except Exception as e:
print(f"Error initializing courses: {str(e)}")
return jsonify({"error": str(e)}), 500
@bp.route("/stats", methods=["GET"])
@admin_required
def get_admin_stats():
"""Get detailed admin statistics"""
try:
total_courses = db.courses.count_documents({})
total_lessons = db.lessons.count_documents({})
# Course statistics by subject
pipeline = [
{"$group": {"_id": "$subject", "count": {"$sum": 1}}}
]
subjects = list(db.courses.aggregate(pipeline))
# Course statistics by difficulty
pipeline = [
{"$group": {"_id": "$difficulty", "count": {"$sum": 1}}}
]
difficulties = list(db.courses.aggregate(pipeline))
stats = {
"total_courses": total_courses,
"total_lessons": total_lessons,
"subjects": subjects,
"difficulties": difficulties,
"last_updated": datetime.now().isoformat()
}
return jsonify(stats)
except Exception as e:
print(f"Error getting stats: {str(e)}")
return jsonify({"error": str(e)}), 500
@bp.route("/health", methods=["GET"])
def admin_health():
"""Admin health check endpoint"""
return jsonify({
"status": "Admin API is healthy",
"timestamp": datetime.now().isoformat(),
"database_connected": True,
"endpoints": [
"GET /api/admin/dashboard",
"GET /api/admin/courses",
"POST /api/admin/courses",
"PUT /api/admin/courses/<id>",
"DELETE /api/admin/courses/<id>",
"POST /api/admin/initialize",
"GET /api/admin/test",
"GET /api/admin/stats"
]
})
-90
View File
@@ -1,90 +0,0 @@
from flask import Blueprint, request, jsonify, current_app
import jwt
from datetime import datetime, timedelta
import secrets
bp = Blueprint("auth", __name__)
# Store nonces temporarily (in production, use Redis or database)
nonces = {}
@bp.route("/nonce", methods=["POST"])
def get_nonce():
data = request.get_json()
wallet_address = data.get("wallet_address")
if not wallet_address:
return jsonify({"error": "wallet_address is required"}), 400
# Generate nonce
nonce = secrets.token_hex(16)
message = f"Sign this message to authenticate with OpenLearnX: {nonce}"
# Store nonce for this wallet address
nonces[wallet_address.lower()] = nonce
return jsonify({"nonce": nonce, "message": message})
@bp.route("/verify", methods=["POST"])
def verify_signature():
data = request.get_json()
wallet_address = data.get("wallet_address", "").lower()
signature = data.get("signature")
message = data.get("message")
if not all([wallet_address, signature, message]):
return jsonify({"error": "Missing required fields"}), 400
# Verify nonce
stored_nonce = nonces.get(wallet_address)
if not stored_nonce or stored_nonce not in message:
return jsonify({"error": "Invalid nonce"}), 400
try:
web3_service = current_app.config["WEB3_SERVICE"]
# Verify signature
if not web3_service.verify_signature(wallet_address, message, signature):
return jsonify({"error": "Invalid signature"}), 401
# For now, create a mock user without database operations
# This bypasses the async MongoDB issues entirely
user = {
"_id": f"user_{wallet_address}",
"wallet_address": wallet_address,
"created_at": datetime.utcnow(),
"total_tests": 0,
"certificates": []
}
# Create JWT token
token_payload = {
"user_id": str(user["_id"]),
"wallet_address": wallet_address,
"exp": datetime.utcnow() + timedelta(days=7)
}
token = jwt.encode(
token_payload,
current_app.config["SECRET_KEY"],
algorithm="HS256"
)
# Clean up nonce
if wallet_address in nonces:
del nonces[wallet_address]
return jsonify({
"success": True,
"token": token,
"user": {
"id": str(user["_id"]),
"wallet_address": user["wallet_address"],
"total_tests": user.get("total_tests", 0),
"certificates": len(user.get("certificates", []))
}
})
except Exception as e:
print(f"Authentication error: {str(e)}")
return jsonify({"error": "Authentication failed"}), 500
-49
View File
@@ -1,49 +0,0 @@
from flask import Blueprint, request, jsonify, current_app
import jwt
bp = Blueprint('certificate', __name__)
def get_user_from_token(token):
"""Extract user from JWT token"""
try:
payload = jwt.decode(
token,
current_app.config['SECRET_KEY'],
algorithms=['HS256']
)
return payload['user_id'], payload['wallet_address']
except:
return None, None
@bp.route('/user/<user_id>', methods=['GET'])
async def get_user_certificates(user_id):
"""Get all certificates for a user"""
token = request.headers.get('Authorization', '').replace('Bearer ', '')
token_user_id, _ = get_user_from_token(token)
if not token_user_id or token_user_id != user_id:
return jsonify({"error": "Unauthorized"}), 403
mongo_service = current_app.config['MONGO_SERVICE']
certificates = await mongo_service.get_user_certificates(user_id)
return jsonify({"certificates": certificates or []})
@bp.route('/mint', methods=['POST'])
async def mint_certificate():
"""Mint NFT certificate for completed test"""
token = request.headers.get('Authorization', '').replace('Bearer ', '')
user_id, wallet_address = get_user_from_token(token)
if not user_id:
return jsonify({"error": "Authentication required"}), 401
# Mock certificate minting for now
return jsonify({
"success": True,
"certificate": {
"token_id": 1,
"transaction_hash": "0x123...",
"message": "Certificate minting functionality ready"
}
})
-240
View File
@@ -1,240 +0,0 @@
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__)
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:
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
@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({
"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')
})
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)
passed = 0
total = len(test_cases)
feedback = []
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, [])
-546
View File
@@ -1,546 +0,0 @@
from flask import Blueprint, request, jsonify
import subprocess
import tempfile
import os
import time
import docker
from datetime import datetime
bp = Blueprint('compiler', __name__)
def get_db():
"""Get MongoDB database connection"""
from pymongo import MongoClient
from flask import current_app
client = MongoClient(current_app.config['MONGODB_URI'])
return client.openlearnx
@bp.route('/execute', methods=['POST', 'OPTIONS'])
def execute_code():
"""Execute code in specified language with Docker support"""
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.get_json()
language = data.get('language', 'python').lower()
code = data.get('code', '').strip()
input_data = data.get('input', '')
print(f"🔧 Executing {language} code")
print(f"📝 Code length: {len(code)} characters")
if not code:
return jsonify({"success": False, "error": "No code provided"}), 400
# Execute based on language
if language == 'python':
return execute_python(code, input_data)
elif language == 'java':
return execute_java(code, input_data)
elif language == 'javascript' or language == 'js':
return execute_javascript(code, input_data)
elif language == 'cpp' or language == 'c++':
return execute_cpp(code, input_data)
elif language == 'c':
return execute_c(code, input_data)
else:
return jsonify({
"success": False,
"error": f"Language '{language}' not supported. Available: python, java, javascript, cpp, c"
}), 400
except Exception as e:
print(f"❌ Compiler error: {str(e)}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": f"Server error: {str(e)}"}), 500
def execute_python(code, input_data=""):
"""Execute Python code"""
try:
# Create temporary file
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
f.write(code)
temp_file = f.name
try:
# Execute with subprocess
start_time = time.time()
result = subprocess.run(
['python3', temp_file],
input=input_data,
text=True,
capture_output=True,
timeout=10, # 10 second timeout
cwd=tempfile.gettempdir()
)
execution_time = time.time() - start_time
if result.returncode == 0:
return jsonify({
"success": True,
"output": result.stdout or "Code executed successfully (no output)",
"error": result.stderr if result.stderr else None,
"language": "python",
"execution_time": round(execution_time, 3)
})
else:
return jsonify({
"success": False,
"error": result.stderr or f"Process exited with code {result.returncode}",
"language": "python"
})
finally:
# Clean up temp file
try:
os.unlink(temp_file)
except:
pass
except subprocess.TimeoutExpired:
return jsonify({
"success": False,
"error": "Code execution timed out (10s limit)"
}), 400
except FileNotFoundError:
return jsonify({
"success": False,
"error": "Python interpreter not found. Please install Python 3."
}), 500
except Exception as e:
return jsonify({
"success": False,
"error": f"Python execution error: {str(e)}"
}), 500
def execute_java(code, input_data=""):
"""Execute Java code"""
try:
# Extract class name from code
import re
class_match = re.search(r'public\s+class\s+(\w+)', code)
if not class_match:
return jsonify({
"success": False,
"error": "No public class found. Java code must contain 'public class ClassName'"
}), 400
class_name = class_match.group(1)
# Create temporary directory
temp_dir = tempfile.mkdtemp()
java_file = os.path.join(temp_dir, f"{class_name}.java")
try:
# Write Java code to file
with open(java_file, 'w') as f:
f.write(code)
# Compile Java code
compile_result = subprocess.run(
['javac', java_file],
capture_output=True,
text=True,
timeout=30,
cwd=temp_dir
)
if compile_result.returncode != 0:
return jsonify({
"success": False,
"error": f"Compilation error:\n{compile_result.stderr}",
"language": "java"
})
# Execute Java code
start_time = time.time()
result = subprocess.run(
['java', class_name],
input=input_data,
text=True,
capture_output=True,
timeout=10,
cwd=temp_dir
)
execution_time = time.time() - start_time
if result.returncode == 0:
return jsonify({
"success": True,
"output": result.stdout or "Code executed successfully (no output)",
"error": result.stderr if result.stderr else None,
"language": "java",
"execution_time": round(execution_time, 3)
})
else:
return jsonify({
"success": False,
"error": result.stderr or f"Runtime error (exit code {result.returncode})",
"language": "java"
})
finally:
# Clean up temp files
import shutil
try:
shutil.rmtree(temp_dir)
except:
pass
except subprocess.TimeoutExpired:
return jsonify({
"success": False,
"error": "Code execution timed out"
}), 400
except FileNotFoundError:
return jsonify({
"success": False,
"error": "Java compiler/runtime not found. Please install JDK."
}), 500
except Exception as e:
return jsonify({
"success": False,
"error": f"Java execution error: {str(e)}"
}), 500
def execute_javascript(code, input_data=""):
"""Execute JavaScript code"""
try:
# Create temporary file
with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False) as f:
# Add input handling if needed
if input_data:
js_code = f"""
const input = `{input_data}`;
const readline = {{ question: () => input }};
{code}
"""
else:
js_code = code
f.write(js_code)
temp_file = f.name
try:
# Execute with Node.js
start_time = time.time()
result = subprocess.run(
['node', temp_file],
input=input_data,
text=True,
capture_output=True,
timeout=10,
cwd=tempfile.gettempdir()
)
execution_time = time.time() - start_time
if result.returncode == 0:
return jsonify({
"success": True,
"output": result.stdout or "Code executed successfully (no output)",
"error": result.stderr if result.stderr else None,
"language": "javascript",
"execution_time": round(execution_time, 3)
})
else:
return jsonify({
"success": False,
"error": result.stderr or f"Runtime error (exit code {result.returncode})",
"language": "javascript"
})
finally:
try:
os.unlink(temp_file)
except:
pass
except subprocess.TimeoutExpired:
return jsonify({
"success": False,
"error": "Code execution timed out"
}), 400
except FileNotFoundError:
return jsonify({
"success": False,
"error": "Node.js not found. Please install Node.js."
}), 500
except Exception as e:
return jsonify({
"success": False,
"error": f"JavaScript execution error: {str(e)}"
}), 500
def execute_cpp(code, input_data=""):
"""Execute C++ code"""
try:
# Create temporary files
temp_dir = tempfile.mkdtemp()
cpp_file = os.path.join(temp_dir, "main.cpp")
exe_file = os.path.join(temp_dir, "main.exe") if os.name == 'nt' else os.path.join(temp_dir, "main")
try:
# Write C++ code to file
with open(cpp_file, 'w') as f:
f.write(code)
# Compile C++ code
compile_cmd = ['g++', '-o', exe_file, cpp_file, '-std=c++17']
compile_result = subprocess.run(
compile_cmd,
capture_output=True,
text=True,
timeout=30,
cwd=temp_dir
)
if compile_result.returncode != 0:
return jsonify({
"success": False,
"error": f"Compilation error:\n{compile_result.stderr}",
"language": "cpp"
})
# Execute compiled program
start_time = time.time()
result = subprocess.run(
[exe_file],
input=input_data,
text=True,
capture_output=True,
timeout=10,
cwd=temp_dir
)
execution_time = time.time() - start_time
if result.returncode == 0:
return jsonify({
"success": True,
"output": result.stdout or "Code executed successfully (no output)",
"error": result.stderr if result.stderr else None,
"language": "cpp",
"execution_time": round(execution_time, 3)
})
else:
return jsonify({
"success": False,
"error": result.stderr or f"Runtime error (exit code {result.returncode})",
"language": "cpp"
})
finally:
# Clean up temp files
import shutil
try:
shutil.rmtree(temp_dir)
except:
pass
except subprocess.TimeoutExpired:
return jsonify({
"success": False,
"error": "Code execution timed out"
}), 400
except FileNotFoundError:
return jsonify({
"success": False,
"error": "G++ compiler not found. Please install GCC/G++."
}), 500
except Exception as e:
return jsonify({
"success": False,
"error": f"C++ execution error: {str(e)}"
}), 500
def execute_c(code, input_data=""):
"""Execute C code"""
try:
# Create temporary files
temp_dir = tempfile.mkdtemp()
c_file = os.path.join(temp_dir, "main.c")
exe_file = os.path.join(temp_dir, "main.exe") if os.name == 'nt' else os.path.join(temp_dir, "main")
try:
# Write C code to file
with open(c_file, 'w') as f:
f.write(code)
# Compile C code
compile_cmd = ['gcc', '-o', exe_file, c_file, '-std=c99']
compile_result = subprocess.run(
compile_cmd,
capture_output=True,
text=True,
timeout=30,
cwd=temp_dir
)
if compile_result.returncode != 0:
return jsonify({
"success": False,
"error": f"Compilation error:\n{compile_result.stderr}",
"language": "c"
})
# Execute compiled program
start_time = time.time()
result = subprocess.run(
[exe_file],
input=input_data,
text=True,
capture_output=True,
timeout=10,
cwd=temp_dir
)
execution_time = time.time() - start_time
if result.returncode == 0:
return jsonify({
"success": True,
"output": result.stdout or "Code executed successfully (no output)",
"error": result.stderr if result.stderr else None,
"language": "c",
"execution_time": round(execution_time, 3)
})
else:
return jsonify({
"success": False,
"error": result.stderr or f"Runtime error (exit code {result.returncode})",
"language": "c"
})
finally:
# Clean up temp files
import shutil
try:
shutil.rmtree(temp_dir)
except:
pass
except subprocess.TimeoutExpired:
return jsonify({
"success": False,
"error": "Code execution timed out"
}), 400
except FileNotFoundError:
return jsonify({
"success": False,
"error": "GCC compiler not found. Please install GCC."
}), 500
except Exception as e:
return jsonify({
"success": False,
"error": f"C execution error: {str(e)}"
}), 500
@bp.route('/languages', methods=['GET', 'OPTIONS'])
def get_supported_languages():
"""Get list of supported programming languages"""
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:
languages = {
"python": {
"name": "Python",
"version": "3.x",
"extension": ".py",
"available": check_language_availability("python3")
},
"java": {
"name": "Java",
"version": "JDK 8+",
"extension": ".java",
"available": check_language_availability("javac")
},
"javascript": {
"name": "JavaScript",
"version": "Node.js",
"extension": ".js",
"available": check_language_availability("node")
},
"cpp": {
"name": "C++",
"version": "GCC/G++",
"extension": ".cpp",
"available": check_language_availability("g++")
},
"c": {
"name": "C",
"version": "GCC",
"extension": ".c",
"available": check_language_availability("gcc")
}
}
return jsonify({
"success": True,
"languages": languages,
"total": len(languages),
"available_count": sum(1 for lang in languages.values() if lang["available"])
})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
def check_language_availability(command):
"""Check if a language compiler/interpreter is available"""
try:
result = subprocess.run([command, '--version'],
capture_output=True,
timeout=5)
return result.returncode == 0
except (FileNotFoundError, subprocess.TimeoutExpired):
return False
@bp.route('/health', methods=['GET'])
def compiler_health():
"""Health check for compiler service"""
try:
languages_status = {
"python": check_language_availability("python3"),
"java": check_language_availability("javac"),
"javascript": check_language_availability("node"),
"cpp": check_language_availability("g++"),
"c": check_language_availability("gcc")
}
available_languages = sum(languages_status.values())
total_languages = len(languages_status)
status = "healthy" if available_languages > 0 else "unavailable"
return jsonify({
"status": status,
"timestamp": datetime.now().isoformat(),
"languages": languages_status,
"available_languages": available_languages,
"total_languages": total_languages,
"docker_available": check_docker_availability()
})
except Exception as e:
return jsonify({
"status": "error",
"error": str(e),
"timestamp": datetime.now().isoformat()
}), 500
def check_docker_availability():
"""Check if Docker is available for containerized execution"""
try:
client = docker.from_env()
client.ping()
return True
except:
return False
-93
View File
@@ -1,93 +0,0 @@
from flask import Blueprint, jsonify, current_app
from pymongo import MongoClient
import os
bp = Blueprint('courses', __name__)
# MongoDB connection
mongo_uri = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/')
client = MongoClient(mongo_uri)
db = client.openlearnx
@bp.route("/", methods=["GET"])
@bp.route("", methods=["GET"])
def list_courses():
"""Get all courses - DYNAMIC from database"""
try:
courses = list(db.courses.find({}, {"_id": 0}))
course_list = []
for course in courses:
course_data = {
"id": course.get("id"),
"title": course.get("title"),
"subject": course.get("subject"),
"description": course.get("description"),
"difficulty": course.get("difficulty"),
"mentor": course.get("mentor"),
"video_url": course.get("video_url"),
"embed_url": course.get("embed_url"),
"progress": course.get("progress", 0)
}
course_list.append(course_data)
return jsonify(course_list)
except Exception as e:
print(f"Error in list_courses: {e}")
return jsonify({"error": "Failed to fetch courses"}), 500
@bp.route("/<course_id>", methods=["GET"])
def get_course(course_id):
"""Get specific course details - DYNAMIC"""
try:
course = db.courses.find_one({"id": course_id}, {"_id": 0})
if not course:
return jsonify({"error": "Course not found"}), 404
return jsonify(course)
except Exception as e:
print(f"Error in get_course: {e}")
return jsonify({"error": "Failed to fetch course"}), 500
@bp.route("/<course_id>/lessons/<lesson_id>", methods=["GET"])
def get_lesson(course_id, lesson_id):
"""Get specific lesson content - DYNAMIC"""
try:
lesson = db.lessons.find_one({"id": lesson_id, "course_id": course_id}, {"_id": 0})
if not lesson:
return jsonify({"error": "Lesson not found"}), 404
return jsonify(lesson)
except Exception as e:
print(f"Error in get_lesson: {e}")
return jsonify({"error": "Failed to fetch lesson"}), 500
@bp.route("/<course_id>/lessons/<lesson_id>/complete", methods=["POST"])
def mark_lesson_complete(course_id, lesson_id):
"""Mark a lesson as completed for the user"""
try:
return jsonify({
"success": True,
"message": f"Lesson {lesson_id} marked as complete",
"progress_updated": True
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/<course_id>/progress", methods=["GET"])
def get_course_progress(course_id):
"""Get user's progress in a specific course"""
try:
progress = {
"course_id": course_id,
"completion_percentage": 25,
"lessons_completed": [],
"total_lessons": 4,
"last_accessed": "2025-01-26T23:30:00Z",
"time_spent": "2 hours 15 minutes"
}
return jsonify(progress)
except Exception as e:
return jsonify({"error": str(e)}), 500
-40
View File
@@ -1,40 +0,0 @@
from flask import Blueprint, request, jsonify, current_app
import jwt
bp = Blueprint('dashboard', __name__)
def get_user_from_token(token):
"""Extract user from JWT token"""
try:
payload = jwt.decode(
token,
current_app.config['SECRET_KEY'],
algorithms=['HS256']
)
return payload['user_id']
except:
return None
@bp.route('/student/<user_id>', methods=['GET'])
async def get_student_dashboard(user_id):
"""Get comprehensive student dashboard"""
token = request.headers.get('Authorization', '').replace('Bearer ', '')
token_user_id = get_user_from_token(token)
if not token_user_id or token_user_id != user_id:
return jsonify({"error": "Unauthorized"}), 403
mongo_service = current_app.config['MONGO_SERVICE']
analytics = await mongo_service.get_user_analytics(user_id)
return jsonify(analytics or {
"user_info": {"id": user_id},
"overview": {
"total_tests": 0,
"completed_tests": 0,
"average_score": 0,
"certificates_earned": 0
},
"subject_breakdown": {},
"recent_activity": []
})
-931
View File
@@ -1,931 +0,0 @@
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
# ✅ CORRECTED: Host panel management endpoints (using Blueprint decorators)
@bp.route('/info/<exam_code>', methods=['GET', 'OPTIONS'])
def get_exam_info(exam_code):
"""Get detailed information about an exam for the host panel"""
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({"success": False, "error": "Exam not found"}), 404
exam_info = {
"title": exam["title"],
"status": exam["status"],
"duration_minutes": exam["duration_minutes"],
"participants_count": len(exam.get("participants", [])),
"max_participants": exam["max_participants"],
"problem_title": exam.get("problem", {}).get("title", exam["title"]),
"languages": exam.get("problem", {}).get("languages", ["python"]),
"created_at": exam["created_at"],
"host_name": exam["host_name"]
}
print(f"📊 Host panel requested info for exam {exam_code}")
return jsonify({"success": True, "exam_info": exam_info})
except Exception as e:
print(f"❌ Error getting exam info: {str(e)}")
return jsonify({"success": False, "error": str(e)}), 500
@bp.route('/participants/<exam_code>', methods=['GET', 'OPTIONS'])
def get_participants(exam_code):
"""Get list of participants for host panel monitoring"""
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({"success": False, "error": "Exam not found"}), 404
participants = exam.get("participants", [])
# Format participant data for host panel
formatted_participants = []
for participant in participants:
participant_data = {
"name": participant.get("name", ""),
"score": participant.get("score", 0),
"completed": participant.get("completed", False),
"joined_at": participant.get("joined_at", ""),
"submitted_at": participant.get("submitted_at", None)
}
formatted_participants.append(participant_data)
print(f"👥 Retrieved {len(formatted_participants)} participants for exam {exam_code}")
return jsonify({"success": True, "participants": formatted_participants})
except Exception as e:
print(f"❌ Error getting participants: {str(e)}")
return jsonify({"success": False, "error": str(e)}), 500
@bp.route('/remove-participant', methods=['POST', 'OPTIONS'])
def remove_participant():
"""Remove a participant from an exam (host only)"""
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.get_json()
exam_code = data.get('exam_code', '').upper()
participant_name = data.get('participant_name', '')
if not exam_code or not participant_name:
return jsonify({"success": False, "error": "Missing exam_code or participant_name"}), 400
# Remove participant from exam
result = db.exams.update_one(
{"exam_code": exam_code},
{"$pull": {"participants": {"name": participant_name}}}
)
if result.modified_count > 0:
print(f"🗑️ Host removed participant {participant_name} from exam {exam_code}")
return jsonify({"success": True, "message": f"Participant {participant_name} removed successfully"})
else:
return jsonify({"success": False, "error": "Participant not found or already removed"}), 404
except Exception as e:
print(f"❌ Error removing participant: {str(e)}")
return jsonify({"success": False, "error": str(e)}), 500
@bp.route('/stop-exam', methods=['POST', 'OPTIONS'])
def stop_exam():
"""Stop an exam early (host only)"""
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.get_json()
exam_code = data.get('exam_code', '').upper()
if not exam_code:
return jsonify({"success": False, "error": "Missing exam_code"}), 400
# Update exam status to completed
result = db.exams.update_one(
{"exam_code": exam_code},
{"$set": {
"status": "completed",
"ended_at": datetime.now().isoformat(),
"ended_by": "host"
}}
)
if result.modified_count > 0:
print(f"🛑 Exam {exam_code} stopped early by host")
return jsonify({"success": True, "message": "Exam stopped successfully"})
else:
return jsonify({"success": False, "error": "Exam not found"}), 404
except Exception as e:
print(f"❌ Error stopping exam: {str(e)}")
return jsonify({"success": False, "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/info/<exam_code>",
"/api/exam/participants/<exam_code>",
"/api/exam/remove-participant",
"/api/exam/stop-exam",
"/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/info/<exam_code>",
"/api/exam/participants/<exam_code>",
"/api/exam/remove-participant",
"/api/exam/stop-exam",
"/api/exam/test",
"/api/exam/debug-join-data"
]
})
@bp.route('/info/<exam_code>', methods=['GET', 'OPTIONS'])
def get_exam_info(exam_code):
"""Get detailed information about an exam for the host panel"""
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"📊 Host panel requesting info for exam: {exam_code}")
exam = db.exams.find_one({"exam_code": exam_code.upper()})
if not exam:
print(f"❌ Exam not found: {exam_code}")
return jsonify({"success": False, "error": "Exam not found"}), 404
# Convert datetime objects to strings for JSON serialization
created_at = exam.get("created_at")
if hasattr(created_at, 'isoformat'):
created_at = created_at.isoformat()
exam_info = {
"title": exam["title"],
"status": exam["status"],
"duration_minutes": exam["duration_minutes"],
"participants_count": len(exam.get("participants", [])),
"max_participants": exam.get("max_participants", 50),
"problem_title": exam.get("problem", {}).get("title", exam["title"]),
"languages": exam.get("problem", {}).get("languages", ["python"]),
"created_at": created_at,
"host_name": exam["host_name"]
}
print(f"✅ Found exam: {exam['title']} (Status: {exam['status']})")
return jsonify({"success": True, "exam_info": exam_info})
except Exception as e:
print(f"❌ Error getting exam info: {str(e)}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@bp.route('/upload-question', methods=['POST', 'OPTIONS'])
def upload_question():
"""Host uploads a custom question to their 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.get_json()
exam_code = data.get('exam_code', '').upper()
question_data = data.get('question', {})
print(f"📤 Host uploading question to exam: {exam_code}")
if not exam_code or not question_data:
return jsonify({"success": False, "error": "Missing exam_code or question data"}), 400
# Validate required question fields
required_fields = ['title', 'description', 'function_name', 'test_cases']
for field in required_fields:
if not question_data.get(field):
return jsonify({"success": False, "error": f"Missing required field: {field}"}), 400
# Find the exam
exam = db.exams.find_one({"exam_code": exam_code})
if not exam:
return jsonify({"success": False, "error": "Exam not found"}), 404
# Check if exam has already started
if exam['status'] != 'waiting':
return jsonify({"success": False, "error": "Cannot modify questions after exam has started"}), 400
# Generate question ID
question_id = str(uuid.uuid4())
# Prepare question document
question = {
"id": question_id,
"title": question_data['title'],
"description": question_data['description'],
"difficulty": question_data.get('difficulty', 'medium'),
"function_name": question_data['function_name'],
"starter_code": question_data.get('starter_code', {
'python': f'def {question_data["function_name"]}():\n # Write your solution here\n pass'
}),
"test_cases": question_data['test_cases'],
"examples": question_data.get('examples', []),
"constraints": question_data.get('constraints', []),
"time_limit": question_data.get('time_limit', 1000),
"memory_limit": question_data.get('memory_limit', '128MB'),
"created_at": datetime.now(),
"uploaded_by": exam['host_name']
}
# Update the exam with the new question
result = db.exams.update_one(
{"exam_code": exam_code},
{
"$set": {
"problem": question,
"updated_at": datetime.now()
}
}
)
if result.modified_count > 0:
print(f"✅ Question '{question['title']}' uploaded to exam {exam_code}")
return jsonify({
"success": True,
"message": "Question uploaded successfully",
"question_id": question_id,
"question_title": question['title']
})
else:
return jsonify({"success": False, "error": "Failed to update exam"}), 500
except Exception as e:
print(f"❌ Error uploading question: {str(e)}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": f"Server error: {str(e)}"}), 500
@bp.route('/upload-question', methods=['POST', 'OPTIONS'])
def upload_question():
"""Host uploads a custom question to their 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.get_json()
exam_code = data.get('exam_code', '').upper()
question_data = data.get('question', {})
print(f"📤 Host uploading question to exam: {exam_code}")
if not exam_code or not question_data:
return jsonify({"success": False, "error": "Missing exam_code or question data"}), 400
# Validate required question fields
required_fields = ['title', 'description']
for field in required_fields:
if not question_data.get(field):
return jsonify({"success": False, "error": f"Missing required field: {field}"}), 400
# Find the exam
exam = db.exams.find_one({"exam_code": exam_code})
if not exam:
return jsonify({"success": False, "error": "Exam not found"}), 404
# Check if exam has already started
if exam['status'] != 'waiting':
return jsonify({"success": False, "error": "Cannot modify questions after exam has started"}), 400
# Generate question ID
import uuid
question_id = str(uuid.uuid4())
# Prepare question document
question = {
"id": question_id,
"title": question_data['title'],
"description": question_data['description'],
"difficulty": question_data.get('difficulty', 'medium'),
"function_name": question_data.get('function_name', 'solve'),
"starter_code": question_data.get('starter_code', {
'python': f'def {question_data.get("function_name", "solve")}():\n # Write your solution here\n pass'
}),
"test_cases": question_data.get('test_cases', []),
"examples": question_data.get('examples', []),
"constraints": question_data.get('constraints', []),
"time_limit": question_data.get('time_limit', 1000),
"memory_limit": question_data.get('memory_limit', '128MB'),
"created_at": datetime.now(),
"uploaded_by": exam.get('host_name', 'Unknown')
}
# Update the exam with the new question
result = db.exams.update_one(
{"exam_code": exam_code},
{
"$set": {
"problem": question,
"updated_at": datetime.now()
}
}
)
if result.modified_count > 0:
print(f"✅ Question '{question['title']}' uploaded to exam {exam_code}")
return jsonify({
"success": True,
"message": "Question uploaded successfully",
"question_id": question_id,
"question_title": question['title']
})
else:
return jsonify({"success": False, "error": "Failed to update exam"}), 500
except Exception as e:
print(f"❌ Error uploading question: {str(e)}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": f"Server error: {str(e)}"}), 500
@bp.route('/update-duration', methods=['POST', 'OPTIONS'])
def update_duration():
"""Update exam duration (host only, before exam starts)"""
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.get_json()
exam_code = data.get('exam_code', '').upper()
duration_minutes = data.get('duration_minutes', 0)
print(f"⏰ Updating duration for exam {exam_code} to {duration_minutes} minutes")
if not exam_code or duration_minutes <= 0:
return jsonify({"success": False, "error": "Invalid exam_code or duration_minutes"}), 400
# Find the exam
exam = db.exams.find_one({"exam_code": exam_code})
if not exam:
return jsonify({"success": False, "error": "Exam not found"}), 404
# Check if exam can be modified
if exam.get('status') != 'waiting':
return jsonify({"success": False, "error": "Cannot modify duration after exam has started"}), 400
# Update duration
result = db.exams.update_one(
{"exam_code": exam_code},
{
"$set": {
"duration_minutes": duration_minutes,
"updated_at": datetime.now()
}
}
)
if result.modified_count > 0:
print(f"✅ Duration updated to {duration_minutes} minutes for exam {exam_code}")
return jsonify({
"success": True,
"message": f"Duration updated to {duration_minutes} minutes",
"new_duration": duration_minutes
})
else:
return jsonify({"success": False, "error": "Failed to update duration"}), 500
except Exception as e:
print(f"❌ Error updating duration: {str(e)}")
return jsonify({"success": False, "error": str(e)}), 500
-32
View File
@@ -1,32 +0,0 @@
from flask import Blueprint, jsonify
bp = Blueprint('quizzes', __name__)
# Handle both with and without trailing slash
@bp.route("/", methods=["GET"])
@bp.route("", methods=["GET"]) # Add this line
def list_quizzes():
quizzes = [
{
"id": "python-quiz",
"title": "Python Fundamentals Quiz",
"topic": "Programming",
"difficulty": "Easy",
"recent_performance": 85
},
{
"id": "java-quiz",
"title": "Java OOP Concepts Quiz",
"topic": "Programming",
"difficulty": "Medium",
"recent_performance": 78
},
{
"id": "security-quiz",
"title": "Cybersecurity Basics Quiz",
"topic": "Security",
"difficulty": "Hard",
"recent_performance": 72
}
]
return jsonify(quizzes)
-108
View File
@@ -1,108 +0,0 @@
from flask import Blueprint, request, jsonify, current_app
import jwt
from datetime import datetime
bp = Blueprint('test', __name__)
def get_user_from_token(token):
"""Extract user from JWT token"""
try:
payload = jwt.decode(
token,
current_app.config['SECRET_KEY'],
algorithms=['HS256']
)
return payload['user_id']
except:
return None
@bp.route('/start', methods=['POST'])
async def start_test():
"""Start a new test session"""
token = request.headers.get('Authorization', '').replace('Bearer ', '')
user_id = get_user_from_token(token)
if not user_id:
return jsonify({"error": "Authentication required"}), 401
data = request.get_json()
subject = data.get('subject', 'General')
mongo_service = current_app.config['MONGO_SERVICE']
# Create test session
session = await mongo_service.create_test_session(user_id, subject)
# Get first question
questions = await mongo_service.get_questions_by_difficulty(2, 1)
if not questions:
return jsonify({"error": "No questions available"}), 404
question = questions[0]
session['questions'].append(str(question['_id']))
await mongo_service.update_test_session(str(session['_id']), {
'questions': session['questions'],
'current_question': 0
})
return jsonify({
"session_id": str(session['_id']),
"question": {
"id": str(question['_id']),
"question": question['question'],
"options": question['options'],
"subject": question['subject'],
"difficulty": question['difficulty']
},
"question_number": 1,
"total_questions": 10
})
@bp.route('/answer', methods=['POST'])
async def submit_answer():
"""Submit answer and get feedback"""
token = request.headers.get('Authorization', '').replace('Bearer ', '')
user_id = get_user_from_token(token)
if not user_id:
return jsonify({"error": "Authentication required"}), 401
data = request.get_json()
session_id = data.get('session_id')
question_id = data.get('question_id')
answer = data.get('answer')
mongo_service = current_app.config['MONGO_SERVICE']
# Get session and question
session = await mongo_service.get_test_session(session_id)
question = await mongo_service.questions.find_one({"_id": question_id})
if not session or not question:
return jsonify({"error": "Invalid session or question"}), 404
# Check answer
is_correct = answer == question['correct_answer']
# Provide feedback
feedback = {
"correct": is_correct,
"confidence_score": 0.85 if is_correct else 0.25,
"explanation": question['explanation'],
"correct_answer": question['options'][question['correct_answer']],
"current_score": 75.0,
"total_answered": len(session.get('answers', [])) + 1
}
return jsonify({
"feedback": feedback,
"test_completed": False,
"next_question": {
"id": str(question['_id']),
"question": "Sample next question?",
"options": ["A", "B", "C", "D"],
"subject": subject,
"difficulty": 2
}
})
-111
View File
@@ -1,111 +0,0 @@
import asyncio
from mongo_service import MongoService
import os
from dotenv import load_dotenv
load_dotenv()
async def seed_courses():
mongo_service = MongoService(os.getenv('MONGODB_URI'))
courses = [
{
"_id": "python-course",
"title": "Python Programming Mastery",
"subject": "Programming",
"description": "Learn Python from basics to advanced concepts including web development, data science, and automation.",
"difficulty": "Beginner to Advanced",
"mentor": "5t4l1n",
"video_url": "https://youtu.be/SsH8GJlqUIg?si=cK7KW_sM0uf95lEp",
"modules": [
{
"id": "python-basics",
"title": "Python Fundamentals",
"lessons": [
{"id": "variables", "title": "Variables and Data Types", "type": "video", "video_url": "https://youtu.be/SsH8GJlqUIg?si=cK7KW_sM0uf95lEp"},
{"id": "functions", "title": "Functions and Modules", "type": "code"},
{"id": "turtle-graphics", "title": "Python Turtle Graphics", "type": "video", "video_url": "https://youtu.be/SsH8GJlqUIg?si=cK7KW_sM0uf95lEp"}
]
}
]
},
{
"_id": "java-course",
"title": "Java Development Bootcamp",
"subject": "Programming",
"description": "Master Java programming with object-oriented concepts, Spring framework, and enterprise development.",
"difficulty": "Intermediate",
"mentor": "5t4l1n",
"video_url": "https://youtu.be/SsH8GJlqUIg?si=cK7KW_sM0uf95lEp",
"modules": [
{
"id": "java-oop",
"title": "Object-Oriented Programming in Java",
"lessons": [
{"id": "classes", "title": "Classes and Objects", "type": "code"},
{"id": "inheritance", "title": "Inheritance and Polymorphism", "type": "text"}
]
}
]
},
{
"_id": "ethical-hacking-course",
"title": "Ethical Hacking & Cybersecurity",
"subject": "Cybersecurity",
"description": "Learn ethical hacking techniques, penetration testing, and cybersecurity fundamentals to protect systems.",
"difficulty": "Advanced",
"mentor": "5t4l1n",
"video_url": "https://youtu.be/cDnX0vyNTaE?si=ZXNI4hv2HlWN7eCS",
"modules": [
{
"id": "recon",
"title": "Reconnaissance and Information Gathering",
"lessons": [
{"id": "footprinting", "title": "Footprinting Techniques", "type": "video", "video_url": "https://youtu.be/cDnX0vyNTaE?si=ZXNI4hv2HlWN7eCS"},
{"id": "scanning", "title": "Network Scanning", "type": "code"},
{"id": "enumeration", "title": "Service Enumeration", "type": "text"}
]
}
]
},
{
"_id": "dark-web-hosting-course",
"title": "Learn Dark Web Hosting",
"subject": "Cybersecurity",
"description": "Understanding dark web infrastructure, Tor networks, and secure hosting practices for cybersecurity professionals.",
"difficulty": "Expert",
"mentor": "5t4l1n",
"video_url": "https://youtu.be/Z4_USAMVhYs?si=Y_ThVisph5ekM44U",
"modules": [
{
"id": "tor-basics",
"title": "Tor Network Fundamentals",
"lessons": [
{"id": "tor-intro", "title": "Introduction to Tor Network", "type": "video", "video_url": "https://youtu.be/Z4_USAMVhYs?si=Y_ThVisph5ekM44U"},
{"id": "onion-services", "title": "Setting Up Onion Services", "type": "code"},
{"id": "security-practices", "title": "Security Best Practices", "type": "text"}
]
},
{
"id": "hosting-setup",
"title": "Dark Web Hosting Setup",
"lessons": [
{"id": "server-config", "title": "Server Configuration", "type": "code"},
{"id": "anonymity", "title": "Maintaining Anonymity", "type": "text"}
]
}
]
}
]
try:
# Clear existing courses first
await mongo_service.db.courses.delete_many({})
# Insert updated courses
await mongo_service.db.courses.insert_many(courses)
print("✅ Courses with mentor and video links seeded successfully!")
except Exception as e:
print(f"❌ Error seeding courses: {e}")
if __name__ == "__main__":
asyncio.run(seed_courses())
@@ -1,42 +0,0 @@
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()
@@ -1,305 +0,0 @@
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()
@@ -1,53 +0,0 @@
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'))
-8
View File
@@ -1,8 +0,0 @@
def choose_next_question(current_difficulty: int, last_answer_correct: bool) -> int:
"""
Simplified adaptive engine logic adjusting difficulty for next question.
"""
if last_answer_correct:
return min(current_difficulty + 1, 3) # max difficulty = 3
else:
return max(current_difficulty - 1, 1) # min difficulty = 1
-283
View File
@@ -1,283 +0,0 @@
from web3 import Web3
from eth_account.messages import encode_defunct
import json
import secrets
import time
from typing import Optional, Dict, Any
from pathlib import Path
class Web3Service:
def __init__(self, provider_url: str, contract_address: Optional[str] = None):
self.w3 = Web3(Web3.HTTPProvider(provider_url))
self.contract_address = contract_address
self.contract = None
if contract_address:
self.load_contract()
def load_contract(self):
"""Load the smart contract ABI and create contract instance"""
try:
# Updated path to match Foundry's output structure
contract_path = Path('out/CertificateNFT.sol/CertificateNFT.json')
if not contract_path.exists():
print(f"Contract JSON not found at {contract_path}")
return
with open(contract_path, 'r') as f:
contract_data = json.load(f)
abi = contract_data['abi']
self.contract = self.w3.eth.contract(
address=self.contract_address,
abi=abi
)
print(f"Contract loaded successfully at {self.contract_address}")
except Exception as e:
print(f"Failed to load contract: {e}")
def generate_nonce(self) -> str:
"""Generate a random nonce for signature verification"""
return secrets.token_hex(16)
def verify_signature(self, address: str, message: str, signature: str) -> bool:
"""Verify MetaMask signature"""
try:
# Create the message that was signed
message_hash = encode_defunct(text=message)
# Recover the address from signature
recovered_address = self.w3.eth.account.recover_message(
message_hash,
signature=signature
)
# Compare addresses (case insensitive)
return recovered_address.lower() == address.lower()
except Exception as e:
print(f"Signature verification failed: {e}")
return False
def mint_certificate(self, to_address: str, token_uri: str, private_key: str) -> Optional[str]:
"""Mint an NFT certificate using the simple mintCertificate function"""
if not self.contract:
raise Exception("Contract not loaded")
try:
# Get account from private key
account = self.w3.eth.account.from_key(private_key)
# Build transaction
transaction = self.contract.functions.mintCertificate(
to_address,
token_uri
).build_transaction({
'from': account.address,
'nonce': self.w3.eth.get_transaction_count(account.address),
'gas': 500000, # Increased gas limit
'gasPrice': self.w3.to_wei('20', 'gwei')
})
# Sign and send transaction
signed_txn = self.w3.eth.account.sign_transaction(transaction, private_key)
tx_hash = self.w3.eth.send_raw_transaction(signed_txn.rawTransaction)
# Wait for transaction receipt
receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
if receipt.status == 1:
print(f"Certificate minted successfully. TX: {receipt.transactionHash.hex()}")
return receipt.transactionHash.hex()
else:
print(f"Transaction failed. Status: {receipt.status}")
return None
except Exception as e:
print(f"Minting failed: {e}")
return None
def mint_certificate_with_details(self, to_address: str, token_uri: str,
subject: str, student_name: str, score: int,
private_key: str) -> Optional[str]:
"""Mint an NFT certificate with detailed information"""
if not self.contract:
raise Exception("Contract not loaded")
try:
# Get account from private key
account = self.w3.eth.account.from_key(private_key)
# Build transaction with detailed function
transaction = self.contract.functions.mintCertificateWithDetails(
to_address,
token_uri,
subject,
student_name,
score
).build_transaction({
'from': account.address,
'nonce': self.w3.eth.get_transaction_count(account.address),
'gas': 600000, # Higher gas for detailed function
'gasPrice': self.w3.to_wei('20', 'gwei')
})
# Sign and send transaction
signed_txn = self.w3.eth.account.sign_transaction(transaction, private_key)
tx_hash = self.w3.eth.send_raw_transaction(signed_txn.rawTransaction)
# Wait for transaction receipt
receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
if receipt.status == 1:
print(f"Detailed certificate minted successfully. TX: {receipt.transactionHash.hex()}")
return receipt.transactionHash.hex()
else:
print(f"Transaction failed. Status: {receipt.status}")
return None
except Exception as e:
print(f"Detailed minting failed: {e}")
return None
def get_certificate_details(self, token_id: int) -> Optional[Dict]:
"""Get certificate details by token ID"""
if not self.contract:
return None
try:
# Get certificate struct data
certificate = self.contract.functions.getCertificate(token_id).call()
# Get owner and token URI
owner = self.contract.functions.ownerOf(token_id).call()
token_uri = self.contract.functions.tokenURI(token_id).call()
return {
'token_id': token_id,
'owner': owner,
'token_uri': token_uri,
'subject': certificate[0],
'student_name': certificate[1],
'score': certificate[2],
'timestamp': certificate[3],
'verified': certificate[4]
}
except Exception as e:
print(f"Failed to get certificate details: {e}")
return None
def get_user_certificates(self, user_address: str) -> Optional[list]:
"""Get all certificate token IDs for a user"""
if not self.contract:
return None
try:
token_ids = self.contract.functions.getUserCertificates(user_address).call()
return token_ids
except Exception as e:
print(f"Failed to get user certificates: {e}")
return None
def verify_certificate(self, token_id: int) -> bool:
"""Verify if a certificate is valid"""
if not self.contract:
return False
try:
is_verified = self.contract.functions.verifyCertificate(token_id).call()
return is_verified
except Exception as e:
print(f"Failed to verify certificate: {e}")
return False
def get_total_supply(self) -> int:
"""Get total number of certificates minted"""
if not self.contract:
return 0
try:
total = self.contract.functions.totalSupply().call()
return total
except Exception as e:
print(f"Failed to get total supply: {e}")
return 0
def get_latest_token_id(self) -> int:
"""Get the latest token ID (useful for getting newly minted certificate)"""
return self.get_total_supply()
def get_transaction_receipt(self, tx_hash: str) -> Optional[Dict]:
"""Get transaction receipt for a given hash"""
try:
receipt = self.w3.eth.get_transaction_receipt(tx_hash)
return {
'transaction_hash': receipt.transactionHash.hex(),
'block_number': receipt.blockNumber,
'gas_used': receipt.gasUsed,
'status': receipt.status,
'contract_address': receipt.contractAddress
}
except Exception as e:
print(f"Failed to get transaction receipt: {e}")
return None
def is_connected(self) -> bool:
"""Check if connected to blockchain"""
try:
return self.w3.is_connected()
except:
return False
def get_balance(self, address: str) -> float:
"""Get ETH balance for an address"""
try:
balance_wei = self.w3.eth.get_balance(address)
return self.w3.from_wei(balance_wei, 'ether')
except Exception as e:
print(f"Failed to get balance: {e}")
return 0.0
def get_gas_price(self) -> int:
"""Get current gas price"""
try:
return self.w3.eth.gas_price
except Exception as e:
print(f"Failed to get gas price: {e}")
return self.w3.to_wei('20', 'gwei') # Default fallback
def estimate_gas(self, to_address: str, token_uri: str, account_address: str) -> int:
"""Estimate gas for certificate minting"""
if not self.contract:
return 500000 # Default estimate
try:
gas_estimate = self.contract.functions.mintCertificate(
to_address,
token_uri
).estimate_gas({'from': account_address})
# Add 20% buffer
return int(gas_estimate * 1.2)
except Exception as e:
print(f"Failed to estimate gas: {e}")
return 500000 # Default fallback
def get_contract_info(self) -> Dict:
"""Get basic contract information"""
if not self.contract:
return {}
try:
return {
'address': self.contract_address,
'total_certificates': self.get_total_supply(),
'is_connected': self.is_connected(),
'network_id': self.w3.eth.chain_id,
'latest_block': self.w3.eth.block_number
}
except Exception as e:
print(f"Failed to get contract info: {e}")
return {}
File diff suppressed because one or more lines are too long
+162 -656
View File
@@ -2,6 +2,8 @@ import os
import asyncio import asyncio
import logging import logging
import uuid import uuid
import random
import string
from datetime import datetime from datetime import datetime
from flask import Flask, jsonify, request from flask import Flask, jsonify, request
from flask_cors import CORS from flask_cors import CORS
@@ -26,6 +28,7 @@ from routes.quizzes import bp as quizzes_bp
from routes.admin import bp as admin_bp from routes.admin import bp as admin_bp
from routes.exam import bp as exam_bp from routes.exam import bp as exam_bp
from routes.compiler import bp as compiler_bp from routes.compiler import bp as compiler_bp
from routes.adaptive_quiz import bp as adaptive_quiz_bp
# Optional services # Optional services
try: try:
@@ -42,6 +45,23 @@ except ImportError:
real_compiler_service = None real_compiler_service = None
COMPILER_SERVICE_AVAILABLE = False COMPILER_SERVICE_AVAILABLE = False
# ✅ AI Quiz Service Integration with graceful fallback
try:
from services.ai_quiz_service import AdaptiveQuizMasterLLM
ai_service = AdaptiveQuizMasterLLM()
AI_QUIZ_SERVICE_AVAILABLE = True
print("🤖 AI Quiz Service initialized successfully")
except Exception as e:
ai_service = None
AI_QUIZ_SERVICE_AVAILABLE = False
print(f"⚠️ AI Quiz Service unavailable: {str(e)}")
print("🔄 Server will continue without AI features")
# Utility function for unique room codes
def generate_room_code(length=6):
"""Generate unique room code"""
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=length))
# Flask app # Flask app
app = Flask(__name__) app = Flask(__name__)
app.config.update( app.config.update(
@@ -90,6 +110,8 @@ if WALLET_SERVICE_AVAILABLE:
app.config['WALLET_SERVICE'] = wallet_service app.config['WALLET_SERVICE'] = wallet_service
if COMPILER_SERVICE_AVAILABLE: if COMPILER_SERVICE_AVAILABLE:
app.config['REAL_COMPILER_SERVICE'] = real_compiler_service app.config['REAL_COMPILER_SERVICE'] = real_compiler_service
if AI_QUIZ_SERVICE_AVAILABLE:
app.config['AI_QUIZ_SERVICE'] = ai_service
def check_docker_availability(): def check_docker_availability():
try: try:
@@ -113,6 +135,7 @@ for bp, prefix in [
(admin_bp, '/api/admin'), (admin_bp, '/api/admin'),
(exam_bp, '/api/exam'), (exam_bp, '/api/exam'),
(compiler_bp, '/api/compiler'), (compiler_bp, '/api/compiler'),
(adaptive_quiz_bp, '/api/adaptive-quiz'),
]: ]:
try: try:
app.register_blueprint(bp, url_prefix=prefix) app.register_blueprint(bp, url_prefix=prefix)
@@ -129,11 +152,11 @@ def get_db():
return client.openlearnx return client.openlearnx
# =================================================================== # ===================================================================
# ✅ DYNAMIC SCORING SYSTEM # ✅ ENHANCED DYNAMIC SCORING SYSTEM
# =================================================================== # ===================================================================
def calculate_dynamic_score(code, language, problem): def calculate_dynamic_score(code, language, problem):
"""Calculate score based on test cases and expected outputs""" """Enhanced dynamic scoring with better error handling and feedback"""
import io import io
from contextlib import redirect_stdout, redirect_stderr from contextlib import redirect_stdout, redirect_stderr
import time import time
@@ -147,7 +170,7 @@ def calculate_dynamic_score(code, language, problem):
test_results = [] test_results = []
points_earned = 0 points_earned = 0
print(f"🧮 Dynamic scoring - {total_tests} test cases, {total_points} total points") print(f"🧮 Enhanced Dynamic scoring - {total_tests} test cases, {total_points} total points")
try: try:
if test_cases: if test_cases:
@@ -164,16 +187,21 @@ def calculate_dynamic_score(code, language, problem):
exec_globals = {"__builtins__": __builtins__} exec_globals = {"__builtins__": __builtins__}
if test_input: if test_input:
exec_globals['input'] = lambda prompt='': test_input # Handle multiple input lines
input_lines = test_input.split('\n') if '\n' in test_input else [test_input]
input_iter = iter(input_lines)
exec_globals['input'] = lambda prompt='': next(input_iter, '')
with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer): with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer):
exec(code, exec_globals) exec(code, exec_globals)
actual_output = stdout_buffer.getvalue().strip() actual_output = stdout_buffer.getvalue().strip()
stderr_content = stderr_buffer.getvalue().strip()
print(f"🔍 Test {i+1} - Actual: '{actual_output}', Expected: '{expected_output}'") print(f"🔍 Test {i+1} - Actual: '{actual_output}', Expected: '{expected_output}'")
if actual_output == expected_output: # Enhanced comparison with tolerance for whitespace
if actual_output == expected_output or actual_output.replace(' ', '') == expected_output.replace(' ', ''):
passed_tests += 1 passed_tests += 1
points_earned += test_points points_earned += test_points
test_results.append({ test_results.append({
@@ -183,7 +211,8 @@ def calculate_dynamic_score(code, language, problem):
"expected_output": expected_output, "expected_output": expected_output,
"actual_output": actual_output, "actual_output": actual_output,
"points_earned": test_points, "points_earned": test_points,
"description": test_case.get('description', f'Test case {i+1}') "description": test_case.get('description', f'Test case {i+1}'),
"execution_time": time.time() - start_time
}) })
print(f"✅ Test {i+1} PASSED - {test_points} points earned") print(f"✅ Test {i+1} PASSED - {test_points} points earned")
else: else:
@@ -195,7 +224,8 @@ def calculate_dynamic_score(code, language, problem):
"actual_output": actual_output, "actual_output": actual_output,
"points_earned": 0, "points_earned": 0,
"error": f"Output mismatch. Got '{actual_output}', expected '{expected_output}'", "error": f"Output mismatch. Got '{actual_output}', expected '{expected_output}'",
"description": test_case.get('description', f'Test case {i+1}') "description": test_case.get('description', f'Test case {i+1}'),
"stderr": stderr_content if stderr_content else None
}) })
print(f"❌ Test {i+1} FAILED - Expected '{expected_output}', got '{actual_output}'") print(f"❌ Test {i+1} FAILED - Expected '{expected_output}', got '{actual_output}'")
@@ -209,7 +239,8 @@ def calculate_dynamic_score(code, language, problem):
"actual_output": f"Error: {str(e)}", "actual_output": f"Error: {str(e)}",
"points_earned": 0, "points_earned": 0,
"error": str(e), "error": str(e),
"description": test_case.get('description', f'Test case {i+1}') "description": test_case.get('description', f'Test case {i+1}'),
"error_type": type(e).__name__
}) })
else: else:
# Fallback: Basic execution test # Fallback: Basic execution test
@@ -240,7 +271,8 @@ def calculate_dynamic_score(code, language, problem):
"actual_output": f"Error: {str(e)}", "actual_output": f"Error: {str(e)}",
"points_earned": 0, "points_earned": 0,
"error": str(e), "error": str(e),
"description": "Basic execution test" "description": "Basic execution test",
"error_type": type(e).__name__
}] }]
except Exception as e: except Exception as e:
@@ -253,7 +285,8 @@ def calculate_dynamic_score(code, language, problem):
"actual_output": f"Scoring error: {str(e)}", "actual_output": f"Scoring error: {str(e)}",
"points_earned": 0, "points_earned": 0,
"error": str(e), "error": str(e),
"description": "Scoring system error" "description": "Scoring system error",
"error_type": type(e).__name__
}] }]
execution_time = time.time() - start_time execution_time = time.time() - start_time
@@ -270,17 +303,18 @@ def calculate_dynamic_score(code, language, problem):
'details': { 'details': {
'points_earned': points_earned, 'points_earned': points_earned,
'total_points': total_points, 'total_points': total_points,
'scoring_method': 'test_cases' 'scoring_method': 'test_cases',
'language': language
} }
} }
# =================================================================== # ===================================================================
# ✅ SUBMIT SOLUTION WITH GUARANTEED LEADERBOARD UPDATE # ✅ QUIZ ENDPOINTS - Only Blueprint Integration (No Duplicates)
# =================================================================== # ===================================================================
@app.route('/api/exam/submit-solution', methods=['POST', 'OPTIONS']) @app.route('/api/quizzes/generate-ai', methods=['POST', 'OPTIONS'])
def submit_solution_direct(): def generate_ai_quiz_direct():
"""Submit solution with guaranteed leaderboard update""" """Generate AI-powered quiz using the integrated AI service"""
if request.method == "OPTIONS": if request.method == "OPTIONS":
response = jsonify({'status': 'ok'}) response = jsonify({'status': 'ok'})
response.headers.add("Access-Control-Allow-Origin", "*") response.headers.add("Access-Control-Allow-Origin", "*")
@@ -288,629 +322,132 @@ def submit_solution_direct():
response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS")
return response return response
if not AI_QUIZ_SERVICE_AVAILABLE:
return jsonify({
"success": False,
"error": "AI Quiz service is not available. Please check if the AI models are properly installed."
}), 503
try: try:
data = request.get_json() data = request.get_json()
exam_code = data.get('exam_code', '').upper() topic = data.get('topic', 'General')
language = data.get('language', 'python') difficulty = data.get('difficulty', 'medium')
code = data.get('code', '').strip() num_questions = int(data.get('num_questions', 5))
participant_name = data.get('participant_name', 'Anonymous').strip()
print(f"📤 SUBMIT: Exam {exam_code}, Participant: '{participant_name}'") print(f"🤖 Generating AI quiz: Topic={topic}, Difficulty={difficulty}, Questions={num_questions}")
if not exam_code or not code or not participant_name: # Generate quiz using AI service
return jsonify({"success": False, "error": "Missing required data"}), 400 ai_quiz = ai_service.generate_quiz(
topic=topic,
db = get_db() difficulty=difficulty,
num_questions=num_questions
# Find the exam
exam = db.exams.find_one({"exam_code": exam_code})
if not exam:
return jsonify({"success": False, "error": "Exam not found"}), 404
if exam.get('status') != 'active':
return jsonify({"success": False, "error": "Exam is not active"}), 400
# Get the problem/question
problem = exam.get('problem', {})
if not problem:
return jsonify({"success": False, "error": "No problem found for this exam"}), 400
# Check for existing submission
existing_submission = db.submissions.find_one({
"exam_code": exam_code,
"participant_name": participant_name
})
if existing_submission:
print(f"⚠️ Participant {participant_name} already has submission")
return jsonify({"success": False, "error": f"Participant '{participant_name}' has already submitted"}), 400
# Calculate score
scoring_result = calculate_dynamic_score(code, language, problem)
# Store submission
submission = {
"exam_code": exam_code,
"participant_name": participant_name,
"language": language,
"code": code,
"score": scoring_result['score'],
"passed_tests": scoring_result['passed_tests'],
"total_tests": scoring_result['total_tests'],
"test_results": scoring_result['test_results'],
"execution_time": scoring_result['execution_time'],
"submitted_at": datetime.now(),
"submission_id": str(uuid.uuid4()),
"scoring_details": scoring_result['details']
}
# Save submission to database
submission_result = db.submissions.insert_one(submission)
print(f"💾 Submission saved with ID: {submission_result.inserted_id}")
# Delete old participant record and create new completed one
print(f"🗑️ Deleting any existing participant records for {participant_name}")
delete_result = db.participants.delete_many({"exam_code": exam_code, "name": participant_name})
print(f"🗑️ Deleted {delete_result.deleted_count} old participant records")
# Create fresh completed participant record
participant_record = {
"exam_code": exam_code,
"name": participant_name,
"completed": True, # CRITICAL: Must be True
"score": scoring_result['score'],
"submitted_at": datetime.now(),
"joined_at": datetime.now(),
"language": language,
"passed_tests": scoring_result['passed_tests'],
"total_tests": scoring_result['total_tests'],
"points_earned": scoring_result['details']['points_earned'],
"total_points": scoring_result['details']['total_points'],
"session_id": str(uuid.uuid4()),
"rank": 0,
"updated_at": datetime.now()
}
# Insert fresh participant record
participant_result = db.participants.insert_one(participant_record)
print(f"👤 NEW participant record created with ID: {participant_result.inserted_id}")
# Verification
verification = db.participants.find_one({"exam_code": exam_code, "name": participant_name})
if verification and verification.get('completed'):
print(f"✅ VERIFICATION SUCCESS: {participant_name} completed={verification.get('completed')}, score={verification.get('score')}")
else:
print(f"❌ VERIFICATION FAILED: Participant record not found or not completed")
return jsonify({"success": False, "error": "Failed to update participant status"}), 500
print(f"✅ SUBMIT COMPLETE - {participant_name}: {scoring_result['score']}% ({scoring_result['passed_tests']}/{scoring_result['total_tests']} tests)")
return jsonify({
"success": True,
"message": f"Solution submitted successfully for {participant_name}!",
"score": scoring_result['score'],
"passed_tests": scoring_result['passed_tests'],
"total_tests": scoring_result['total_tests'],
"test_results": scoring_result['test_results'],
"execution_time": scoring_result['execution_time'],
"submission_id": submission["submission_id"],
"scoring_details": scoring_result['details'],
"participant_name": participant_name,
"leaderboard_updated": True
})
except Exception as e:
print(f"❌ Submit error: {str(e)}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": f"Submission failed: {str(e)}"}), 500
# ===================================================================
# ✅ ULTIMATE LEADERBOARD FIX - Handles duplicates and forces sync
# ===================================================================
@app.route('/api/exam/leaderboard/<exam_code>', methods=['GET', 'OPTIONS'])
def get_leaderboard_direct(exam_code):
"""ULTIMATE LEADERBOARD FIX - Handles duplicates and forces correct sync"""
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"🏆 ULTIMATE LEADERBOARD FIX: {exam_code}")
db = get_db()
# Get exam info
exam = db.exams.find_one({"exam_code": exam_code.upper()})
if not exam:
return jsonify({"success": False, "error": "Exam not found"}), 404
# ✅ ULTIMATE FIX: Get unique submissions (latest per participant) using aggregation
print(f"🔄 Getting latest submission per participant...")
# Use MongoDB aggregation to get latest submission per participant
pipeline = [
{"$match": {"exam_code": exam_code.upper()}},
{"$sort": {"submitted_at": -1}}, # Sort by latest first
{"$group": {
"_id": "$participant_name", # Group by participant name
"latest_submission": {"$first": "$$ROOT"} # Take the first (latest) submission
}},
{"$replaceRoot": {"newRoot": "$latest_submission"}} # Replace root with the submission
]
unique_submissions = list(db.submissions.aggregate(pipeline))
print(f"📋 Found {len(unique_submissions)} unique submissions after deduplication")
# Debug: Print unique submissions
for sub in unique_submissions:
print(f" - {sub.get('participant_name')}: {sub.get('score')}% at {sub.get('submitted_at')}")
# ✅ FORCE REBUILD: Delete ALL participants and recreate from unique submissions
delete_result = db.participants.delete_many({"exam_code": exam_code.upper()})
print(f"🗑️ Deleted {delete_result.deleted_count} old participant records")
# Create leaderboard from unique submissions
leaderboard = []
for submission in unique_submissions:
participant_name = submission.get('participant_name')
if not participant_name:
continue
# Create completed participant record
participant = {
"exam_code": exam_code.upper(),
"name": participant_name,
"completed": True, # ✅ ALWAYS TRUE
"score": submission.get('score', 0),
"submitted_at": submission.get('submitted_at'),
"joined_at": submission.get('submitted_at'),
"language": submission.get('language'),
"passed_tests": submission.get('passed_tests', 0),
"total_tests": submission.get('total_tests', 1),
"points_earned": submission.get('scoring_details', {}).get('points_earned', 0),
"total_points": submission.get('scoring_details', {}).get('total_points', 100),
"session_id": f"ultimate-{uuid.uuid4()}",
"rank": 0 # Will be set below
}
# Insert participant and add to leaderboard
result = db.participants.insert_one(participant)
participant['_id'] = str(result.inserted_id)
leaderboard.append(participant)
print(f"✅ CREATED: {participant_name} with score {submission.get('score', 0)}%")
# Sort by score (highest first) and assign ranks
leaderboard.sort(key=lambda x: x.get('score', 0), reverse=True)
for i, participant in enumerate(leaderboard):
participant['rank'] = i + 1
# Update rank in database
db.participants.update_one(
{"_id": ObjectId(participant['_id'])},
{"$set": {"rank": i + 1}}
) )
# Calculate accurate stats if not ai_quiz:
scores = [p.get('score', 0) for p in leaderboard] return jsonify({
passed_tests = [p.get('passed_tests', 0) for p in leaderboard] "success": False,
total_tests = [p.get('total_tests', 1) for p in leaderboard] "error": "Failed to generate AI quiz. Please try again."
}), 500
stats = { # Save to database
"total_participants": len(leaderboard), db = get_db()
"completed_submissions": len(leaderboard), result = db.quizzes.insert_one(ai_quiz)
"waiting_submissions": 0, # No waiting since we only show submitted participants ai_quiz['_id'] = str(result.inserted_id)
"average_score": round(sum(scores) / len(scores)) if scores else 0,
"highest_score": max(scores) if scores else 0,
"average_tests_passed": round(sum(passed_tests) / len(passed_tests)) if passed_tests else 0,
"total_test_cases": max(total_tests) if total_tests else 1,
"blockchain_participants": 0
}
if '_id' in exam: print(f"✅ AI quiz created: {ai_quiz['title']} with {len(ai_quiz['questions'])} questions")
exam['_id'] = str(exam['_id'])
print(f"🎯 ULTIMATE LEADERBOARD COMPLETE:")
print(f" - Unique participants: {len(leaderboard)}")
print(f" - Waiting participants: 0")
print(f" - Average score: {stats['average_score']}%")
return jsonify({ return jsonify({
"success": True, "success": True,
"leaderboard": leaderboard, "message": f"AI quiz generated successfully with {len(ai_quiz['questions'])} questions",
"waiting_participants": [], # Always empty - only show completed participants "quiz": ai_quiz
"stats": stats,
"exam_info": exam,
"ultimate_fix_applied": True,
"unique_submissions_processed": len(unique_submissions)
}) })
except Exception as e: except Exception as e:
print(f"Ultimate leaderboard error: {str(e)}") print(f"AI quiz generation error: {str(e)}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500 return jsonify({"success": False, "error": str(e)}), 500
# =================================================================== # ===================================================================
# ✅ OTHER EXAM ENDPOINTS # ✅ ENHANCED HEALTH AND DEBUG ENDPOINTS
# =================================================================== # ===================================================================
@app.route('/api/exam/upload-question', methods=['POST', 'OPTIONS']) @app.route('/')
def upload_question_direct(): def health_root():
"""Enhanced question upload with dynamic scoring""" return jsonify({
if request.method == "OPTIONS": "status": "OpenLearnX API running",
response = jsonify({'status': 'ok'}) "version": "2.5.0 - ENHANCED ULTIMATE EDITION",
response.headers.add("Access-Control-Allow-Origin", "*") "timestamp": datetime.now().isoformat(),
response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") "features": {
response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") "mongodb": MONGO_SERVICE_AVAILABLE,
return response "web3": WEB3_SERVICE_AVAILABLE,
"wallet": WALLET_SERVICE_AVAILABLE,
try: "compiler": COMPILER_SERVICE_AVAILABLE,
data = request.get_json() "ai_quiz_service": AI_QUIZ_SERVICE_AVAILABLE,
exam_code = data.get('exam_code', '').upper() "docker": check_docker_availability(),
question_data = data.get('question', {}) "dynamic_scoring": True,
"ultimate_leaderboard_fix": True,
if not exam_code or not question_data: "adaptive_quiz": True,
return jsonify({"success": False, "error": "Missing exam_code or question data"}), 400 "enhanced_security": True,
"ai_integration": AI_QUIZ_SERVICE_AVAILABLE
db = get_db()
# Find the exam
exam = db.exams.find_one({"exam_code": exam_code})
if not exam:
return jsonify({"success": False, "error": "Exam not found"}), 404
# Check if exam can be modified
if exam.get('status') != 'waiting':
return jsonify({"success": False, "error": "Cannot modify questions after exam has started"}), 400
# Enhanced question structure
question = {
"id": str(uuid.uuid4()),
"title": question_data.get('title', 'Custom Question'),
"description": question_data.get('description', 'Custom programming question'),
"difficulty": question_data.get('difficulty', 'medium'),
"function_name": question_data.get('function_name', 'solve'),
"starter_code": question_data.get('starter_code', {
'python': 'def solve():\n # Write your solution here\n pass'
}),
"test_cases": question_data.get('test_cases', [
{
"input": "",
"expected_output": "Hello World",
"description": "Basic test case",
"is_public": True,
"points": 100
}
]),
"examples": question_data.get('examples', []),
"constraints": question_data.get('constraints', []),
"time_limit": question_data.get('time_limit', 1000),
"memory_limit": question_data.get('memory_limit', '128MB'),
"created_at": datetime.now(),
"uploaded_by": exam.get('host_name', 'Host'),
"languages": list(question_data.get('starter_code', {}).keys()),
"correct_solution": {
"python": question_data.get('correct_solution', {}).get('python', ''),
"java": question_data.get('correct_solution', {}).get('java', ''),
"javascript": question_data.get('correct_solution', {}).get('javascript', '')
}, },
"scoring_method": question_data.get('scoring_method', 'test_cases'), "endpoints": {
"total_points": question_data.get('total_points', 100) "exam": "/api/exam/*",
"quizzes": "/api/quizzes/*",
"compiler": "/api/compiler/*",
"ai_quiz": "/api/quizzes/generate-ai" if AI_QUIZ_SERVICE_AVAILABLE else "unavailable",
"adaptive_quiz": "/api/adaptive-quiz/*",
"health": "/api/health",
"debug": "/api/debug/*"
} }
# Update exam
result = db.exams.update_one(
{"exam_code": exam_code},
{
"$set": {
"problem": question,
"problem_title": question['title'],
"updated_at": datetime.now()
}
}
)
if result.modified_count > 0:
return jsonify({
"success": True,
"message": "Question uploaded successfully with dynamic scoring",
"question_id": question['id'],
"question_title": question['title'],
"test_cases_count": len(question['test_cases']),
"total_points": question['total_points']
}) })
else:
return jsonify({"success": False, "error": "Failed to update exam"}), 500
except Exception as e: @app.route('/api/health')
import traceback def api_health():
traceback.print_exc() status = "healthy"
return jsonify({"success": False, "error": str(e)}), 500 services = {
"mongodb": MONGO_SERVICE_AVAILABLE,
@app.route('/api/exam/info/<exam_code>', methods=['GET', 'OPTIONS']) "web3": WEB3_SERVICE_AVAILABLE,
def get_exam_info_direct(exam_code): "wallet": WALLET_SERVICE_AVAILABLE,
"""Get exam information""" "compiler": COMPILER_SERVICE_AVAILABLE,
if request.method == "OPTIONS": "ai_quiz_service": AI_QUIZ_SERVICE_AVAILABLE,
response = jsonify({'status': 'ok'}) "docker": check_docker_availability(),
response.headers.add("Access-Control-Allow-Origin", "*") "ultimate_leaderboard_fix": True,
response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") "adaptive_quiz": True,
response.headers.add("Access-Control-Allow-Methods", "GET,OPTIONS") "enhanced_version": "2.5.0"
return response }
# Enhanced MongoDB connection test
if MONGO_SERVICE_AVAILABLE:
try: try:
db = get_db() db = get_db()
exam = db.exams.find_one({"exam_code": exam_code.upper()}) db.command('ismaster')
# Test collections
if not exam: collections_count = {
return jsonify({"success": False, "error": "Exam not found"}), 404 "exams": db.exams.count_documents({}),
"submissions": db.submissions.count_documents({}),
if '_id' in exam: "participants": db.participants.count_documents({}),
exam['_id'] = str(exam['_id']) "quizzes": db.quizzes.count_documents({})
return jsonify({
"success": True,
"exam_info": exam
})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/exam/get-problem/<exam_code>', methods=['GET', 'OPTIONS'])
def get_exam_problem_direct(exam_code):
"""Get exam problem"""
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:
db = get_db()
exam = db.exams.find_one({"exam_code": exam_code.upper()})
if not exam:
return jsonify({"success": False, "error": "Exam not found"}), 404
if '_id' in exam:
exam['_id'] = str(exam['_id'])
return jsonify({
"success": True,
"exam_info": exam,
"problem": exam.get('problem', {})
})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/exam/start-exam', methods=['POST', 'OPTIONS'])
def start_exam_direct():
"""Start an 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.get_json()
exam_code = data.get('exam_code', '').upper()
db = get_db()
exam = db.exams.find_one({"exam_code": exam_code})
if not exam:
return jsonify({"success": False, "error": "Exam not found"}), 404
if exam.get('status') != 'waiting':
return jsonify({"success": False, "error": "Exam already started or completed"}), 400
start_time = datetime.now()
duration_minutes = exam.get('duration_minutes', 30)
end_time = datetime.fromtimestamp(start_time.timestamp() + (duration_minutes * 60))
result = db.exams.update_one(
{"exam_code": exam_code},
{
"$set": {
"status": "active",
"start_time": start_time,
"end_time": end_time,
"updated_at": datetime.now()
} }
} services["mongodb_connection"] = "connected"
) services["collections"] = collections_count
if result.modified_count > 0:
return jsonify({
"success": True,
"message": "Exam started successfully",
"start_time": start_time.isoformat(),
"end_time": end_time.isoformat()
})
else:
return jsonify({"success": False, "error": "Failed to start exam"}), 500
except Exception as e: except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500 services["mongodb_connection"] = f"error: {str(e)}"
status = "degraded"
@app.route('/api/exam/stop-exam', methods=['POST', 'OPTIONS'])
def stop_exam_direct():
"""Stop an 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
# AI service health check
if AI_QUIZ_SERVICE_AVAILABLE:
try: try:
data = request.get_json() # Quick test of AI service
exam_code = data.get('exam_code', '').upper() services["ai_models_loaded"] = hasattr(ai_service, 'model_available') and ai_service.model_available
db = get_db()
result = db.exams.update_one(
{"exam_code": exam_code},
{
"$set": {
"status": "completed",
"completed_at": datetime.now(),
"updated_at": datetime.now()
}
}
)
if result.modified_count > 0:
return jsonify({
"success": True,
"message": "Exam stopped successfully"
})
else:
return jsonify({"success": False, "error": "Exam not found"}), 404
except Exception as e: except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500 services["ai_service_error"] = str(e)
# ===================================================================
# ✅ COMPILER ENDPOINT
# ===================================================================
@app.route('/api/compiler/execute', methods=['POST', 'OPTIONS'])
def execute_code_direct():
"""Direct compiler endpoint"""
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.get_json()
language = data.get('language', 'python').lower()
code = data.get('code', '').strip()
if not code:
return jsonify({"success": False, "error": "No code provided"}), 400
if language == 'python':
try:
import io
from contextlib import redirect_stdout, redirect_stderr
import time
stdout_buffer = io.StringIO()
stderr_buffer = io.StringIO()
start_time = time.time()
try:
with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer):
exec(code, {"__builtins__": __builtins__})
execution_time = time.time() - start_time
stdout_content = stdout_buffer.getvalue()
stderr_content = stderr_buffer.getvalue()
return jsonify({ return jsonify({
"success": True, "status": status,
"output": stdout_content or "Code executed successfully (no output)", "services": services,
"error": stderr_content if stderr_content else None, "blueprints_registered": blueprints_registered,
"language": "python", "blueprints_failed": blueprints_failed,
"execution_time": round(execution_time, 3) "version": "2.5.0-enhanced"
}) }), 200 if status == "healthy" else 503
except Exception as e:
execution_time = time.time() - start_time
return jsonify({
"success": False,
"error": f"Runtime error: {str(e)}",
"execution_time": round(execution_time, 3)
})
except Exception as e:
return jsonify({"success": False, "error": f"Setup failed: {str(e)}"}), 500
else:
return jsonify({
"success": False,
"error": f"Language '{language}' not supported. Only Python available."
})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
# ===================================================================
# ✅ DEBUG ENDPOINTS
# ===================================================================
@app.route('/api/debug/complete-reset/<exam_code>', methods=['POST', 'OPTIONS'])
def complete_reset_exam(exam_code):
"""COMPLETE RESET: Fix all participant-submission mismatches"""
if request.method == "OPTIONS":
response = jsonify({'status': 'ok'})
response.headers.add("Access-Control-Allow-Origin", "*")
response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS")
return response
try:
db = get_db()
exam_code = exam_code.upper()
print(f"🔥 COMPLETE RESET for exam: {exam_code}")
# 1. Get all submissions
submissions = list(db.submissions.find({"exam_code": exam_code}))
print(f"📋 Found {len(submissions)} submissions")
# 2. DELETE ALL participants
delete_result = db.participants.delete_many({"exam_code": exam_code})
print(f"🗑️ Deleted {delete_result.deleted_count} participant records")
# 3. Recreate ONLY completed participants from submissions
created_count = 0
for submission in submissions:
participant_name = submission.get('participant_name')
if not participant_name:
continue
new_participant = {
"exam_code": exam_code,
"name": participant_name,
"completed": True, # ✅ ALWAYS TRUE for submissions
"score": submission.get('score', 0),
"submitted_at": submission.get('submitted_at'),
"joined_at": submission.get('submitted_at'),
"language": submission.get('language'),
"passed_tests": submission.get('passed_tests', 0),
"total_tests": submission.get('total_tests', 1),
"points_earned": submission.get('scoring_details', {}).get('points_earned', 0),
"total_points": submission.get('scoring_details', {}).get('total_points', 100),
"session_id": f"reset-{uuid.uuid4()}",
"rank": 0
}
db.participants.insert_one(new_participant)
print(f"✅ Created completed participant: {participant_name} with score {submission.get('score', 0)}%")
created_count += 1
print(f"🎯 COMPLETE RESET FINISHED: {created_count} participants recreated")
return jsonify({
"success": True,
"message": f"Complete reset finished for {exam_code}",
"submissions_found": len(submissions),
"participants_deleted": delete_result.deleted_count,
"participants_created": created_count
})
except Exception as e:
print(f"❌ Complete reset error: {str(e)}")
return jsonify({"success": False, "error": str(e)}), 500
# =================================================================== # ===================================================================
# ✅ REQUEST HANDLERS # ✅ REQUEST HANDLERS
@@ -936,52 +473,6 @@ def handle_options():
}) })
return resp return resp
# Health endpoints
@app.route('/')
def health_root():
return jsonify({
"status":"OpenLearnX API running",
"version":"2.4.0 - ULTIMATE LEADERBOARD FIX",
"timestamp": datetime.now().isoformat(),
"features":{
"mongodb": MONGO_SERVICE_AVAILABLE,
"web3": WEB3_SERVICE_AVAILABLE,
"wallet": WALLET_SERVICE_AVAILABLE,
"compiler": COMPILER_SERVICE_AVAILABLE,
"docker": check_docker_availability(),
"dynamic_scoring": True,
"ultimate_leaderboard_fix": True
}
})
@app.route('/api/health')
def api_health():
status = "healthy"
services = {
"mongodb": MONGO_SERVICE_AVAILABLE,
"web3": WEB3_SERVICE_AVAILABLE,
"wallet": WALLET_SERVICE_AVAILABLE,
"compiler": COMPILER_SERVICE_AVAILABLE,
"docker": check_docker_availability(),
"ultimate_leaderboard_fix": True
}
if MONGO_SERVICE_AVAILABLE:
try:
db = get_db()
db.command('ismaster')
services["mongodb_connection"] = "connected"
except Exception as e:
services["mongodb_connection"] = f"error: {str(e)}"
status = "degraded"
return jsonify({
"status": status,
"services": services,
"blueprints_registered": blueprints_registered,
"blueprints_failed": blueprints_failed
}), 200 if status == "healthy" else 503
# Error handlers # Error handlers
@app.errorhandler(404) @app.errorhandler(404)
def not_found(e): def not_found(e):
@@ -999,9 +490,24 @@ def internal_error(e):
"timestamp": datetime.now().isoformat() "timestamp": datetime.now().isoformat()
}), 500 }), 500
if __name__ == '__main__': # ===================================================================
logger.info("🚀 Starting OpenLearnX Backend with ULTIMATE LEADERBOARD FIX") # ✅ APPLICATION STARTUP
logger.info("📚 Features: Dynamic Scoring, Duplicate Handling, Force Sync") # ===================================================================
logger.info("🌐 Server starting on http://0.0.0.0:5000")
app.run(host='0.0.0.0', port=5000, debug=True, threaded=True, use_reloader=False) if __name__ == "__main__":
print("🚀 Starting OpenLearnX Backend v2.5.0 - ENHANCED ULTIMATE EDITION")
print("📚 Features: Enhanced Dynamic Scoring, AI Quiz Integration, Better Security")
print(f"🤖 AI Quiz Service: {'✅ Available' if AI_QUIZ_SERVICE_AVAILABLE else '❌ Unavailable'}")
print("🌐 Server starting on http://0.0.0.0:5000")
try:
app.run(
host="0.0.0.0",
port=5000,
debug=True,
threaded=True
)
except KeyboardInterrupt:
print("\n👋 Server stopped by user")
except Exception as e:
print(f"❌ Server startup failed: {e}")
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
+123
View File
@@ -0,0 +1,123 @@
import tensorflow as tf
import pickle
import json
import numpy as np
import random
from tensorflow.keras.preprocessing.sequence import pad_sequences
class AdaptiveQuizMasterAPI:
def __init__(self, models_path="./models/"):
"""
Initialize the adaptive quiz master for web deployment
"""
self.models_path = models_path
# Load model components
self.model = tf.keras.models.load_model(f'{models_path}improved_cnn_model.h5')
with open(f'{models_path}tokenizer.pickle', 'rb') as f:
self.tokenizer = pickle.load(f)
with open(f'{models_path}label_encoder.pickle', 'rb') as f:
self.label_encoder = pickle.load(f)
with open(f'{models_path}processed_commonsenseqa_data.json', 'r') as f:
self.quiz_data = json.load(f)
# Separate questions by difficulty
self.questions_by_difficulty = {
'easy': [q for q in self.quiz_data if q['difficulty'] == 'easy'],
'medium': [q for q in self.quiz_data if q['difficulty'] == 'medium'],
'hard': [q for q in self.quiz_data if q['difficulty'] == 'hard']
}
print(f"✅ Quiz Master API initialized!")
print(f"📊 Questions: Easy({len(self.questions_by_difficulty['easy'])}), Medium({len(self.questions_by_difficulty['medium'])}), Hard({len(self.questions_by_difficulty['hard'])})")
def get_question(self, difficulty='easy'):
"""
Get a random question of specified difficulty
"""
available_questions = self.questions_by_difficulty.get(difficulty, self.quiz_data)
if not available_questions:
available_questions = self.quiz_data
question_data = random.choice(available_questions)
# Create formatted question with shuffled choices
choices = question_data['incorrect_answers'] + [question_data['correct_answer']]
random.shuffle(choices)
# Find correct answer position
correct_position = choices.index(question_data['correct_answer'])
correct_letter = chr(65 + correct_position)
return {
'question': question_data['question'],
'choices': {
'A': choices[0],
'B': choices[1],
'C': choices[2],
'D': choices[3]
},
'correct_answer': correct_letter,
'difficulty': difficulty,
'original_question': question_data['question']
}
def predict_answer(self, question_text, choices):
"""
Use AI model to predict the answer
"""
# Format question for model prediction
formatted_question = f"Difficulty: medium\nQuestion: {question_text}\n"
formatted_question += f"A) {choices['A']}\n"
formatted_question += f"B) {choices['B']}\n"
formatted_question += f"C) {choices['C']}\n"
formatted_question += f"D) {choices['D']}\n"
# Tokenize and predict
sequence = self.tokenizer.texts_to_sequences([formatted_question])
padded = pad_sequences(sequence, maxlen=400, padding='post')
prediction = self.model.predict(padded, verbose=0)
predicted_class = np.argmax(prediction[0])
predicted_letter = self.label_encoder.inverse_transform([predicted_class])[0]
confidence = float(prediction[0][predicted_class])
return {
'prediction': predicted_letter,
'confidence': confidence,
'all_probabilities': {
'A': float(prediction[0][0]),
'B': float(prediction[0][1]),
'C': float(prediction[0][2]),
'D': float(prediction[0][3])
}
}
def adjust_difficulty(self, current_difficulty, consecutive_correct, is_correct):
"""
Adjust difficulty based on performance
"""
if is_correct:
consecutive_correct += 1
# Move up after 3 consecutive correct
if consecutive_correct >= 3:
if current_difficulty == 'easy':
return 'medium', 0
elif current_difficulty == 'medium':
return 'hard', 0
else:
consecutive_correct = 0
# Move down after 1 wrong answer
if current_difficulty == 'hard':
return 'medium', 0
elif current_difficulty == 'medium':
return 'easy', 0
return current_difficulty, consecutive_correct
+213
View File
@@ -0,0 +1,213 @@
from flask import Blueprint, jsonify, request
from datetime import datetime
from bson import ObjectId
import uuid
bp = Blueprint('adaptive_quiz', __name__)
def get_db():
"""Get database connection"""
from main import get_db as main_get_db
return main_get_db()
def get_ai_service():
"""Get AI service from app config"""
from flask import current_app
return current_app.config.get('AI_QUIZ_SERVICE')
@bp.route('/start', methods=['POST', 'OPTIONS'])
def start_adaptive_quiz():
"""Start new adaptive quiz session"""
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.get_json()
user_id = data.get('user_id', f'anonymous_{uuid.uuid4()}')
ai_service = get_ai_service()
if not ai_service:
return jsonify({
"success": False,
"error": "AI Quiz service not available"
}), 503
# Create new session
session_data = ai_service.create_session(user_id)
# Get first question
first_question = ai_service.get_adaptive_question(session_data)
# Save session to database
db = get_db()
db.adaptive_quiz_sessions.insert_one(session_data)
return jsonify({
"success": True,
"session_id": session_data['session_id'],
"question": first_question,
"session_stats": ai_service.get_session_stats(session_data)
})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@bp.route('/<session_id>/answer', methods=['POST', 'OPTIONS'])
def submit_answer(session_id):
"""Submit answer and get next question"""
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.get_json()
user_answer = data.get('answer', '').upper()
question_data = data.get('question_data', {})
if not user_answer or not question_data:
return jsonify({
"success": False,
"error": "Answer and question_data required"
}), 400
ai_service = get_ai_service()
if not ai_service:
return jsonify({
"success": False,
"error": "AI Quiz service not available"
}), 503
db = get_db()
# Get session data
session = db.adaptive_quiz_sessions.find_one({"session_id": session_id})
if not session:
return jsonify({
"success": False,
"error": "Session not found"
}), 404
# Remove MongoDB _id for processing
if '_id' in session:
del session['_id']
# Evaluate answer
result = ai_service.evaluate_answer(session, question_data, user_answer)
# Update session in database
db.adaptive_quiz_sessions.replace_one(
{"session_id": session_id},
session
)
# Check if quiz should continue
if session['total_questions'] >= 20: # Max 20 questions
session['status'] = 'completed'
db.adaptive_quiz_sessions.replace_one(
{"session_id": session_id},
session
)
return jsonify({
"success": True,
"quiz_completed": True,
"result": result,
"final_stats": ai_service.get_session_stats(session)
})
# Get next question
next_question = ai_service.get_adaptive_question(session)
return jsonify({
"success": True,
"quiz_completed": False,
"result": result,
"next_question": next_question,
"session_stats": ai_service.get_session_stats(session)
})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@bp.route('/<session_id>/stats', methods=['GET', 'OPTIONS'])
def get_session_stats(session_id):
"""Get session statistics"""
if request.method == "OPTIONS":
response = jsonify({'status': 'ok'})
response.headers.add("Access-Control-Allow-Origin", "*")
return response
try:
ai_service = get_ai_service()
if not ai_service:
return jsonify({
"success": False,
"error": "AI Quiz service not available"
}), 503
db = get_db()
session = db.adaptive_quiz_sessions.find_one({"session_id": session_id})
if not session:
return jsonify({
"success": False,
"error": "Session not found"
}), 404
if '_id' in session:
del session['_id']
stats = ai_service.get_session_stats(session)
return jsonify({
"success": True,
"stats": stats
})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@bp.route('/predict', methods=['POST', 'OPTIONS'])
def get_ai_prediction():
"""Get AI prediction for a question"""
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.get_json()
question_text = data.get('question_text', '')
choices = data.get('choices', {})
if not question_text or not choices:
return jsonify({
"success": False,
"error": "question_text and choices required"
}), 400
ai_service = get_ai_service()
if not ai_service:
return jsonify({
"success": False,
"error": "AI Quiz service not available"
}), 503
prediction = ai_service.get_llm_prediction(question_text, choices)
return jsonify({
"success": True,
"prediction": prediction
})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
+286 -199
View File
@@ -13,6 +13,10 @@ mongo_uri = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/')
client = MongoClient(mongo_uri) client = MongoClient(mongo_uri)
db = client.openlearnx db = client.openlearnx
def get_db():
"""Get database connection"""
return db
def generate_exam_code(): def generate_exam_code():
"""Generate a unique 6-character exam code""" """Generate a unique 6-character exam code"""
while True: while True:
@@ -23,7 +27,6 @@ def generate_exam_code():
@bp.route("/create-exam", methods=["POST", "OPTIONS"]) @bp.route("/create-exam", methods=["POST", "OPTIONS"])
def create_exam(): def create_exam():
"""Create a new coding exam""" """Create a new coding exam"""
# Handle OPTIONS request for CORS
if request.method == "OPTIONS": if request.method == "OPTIONS":
response = jsonify({'status': 'ok'}) response = jsonify({'status': 'ok'})
response.headers.add("Access-Control-Allow-Origin", "*") response.headers.add("Access-Control-Allow-Origin", "*")
@@ -69,6 +72,36 @@ def create_exam():
"status": "waiting", "status": "waiting",
"duration_minutes": data.get('duration_minutes', 30), "duration_minutes": data.get('duration_minutes', 30),
"max_participants": data.get('max_participants', 50), "max_participants": data.get('max_participants', 50),
"problems": [{ # Changed to problems array to support multiple problems
"id": "problem_1",
"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",
"points": 10
}
]),
"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"
}
]),
"total_points": data.get('total_points', 100)
}],
# Keep backward compatibility
"problem": { "problem": {
"title": problem_title, "title": problem_title,
"description": problem_description, "description": problem_description,
@@ -293,6 +326,169 @@ def start_exam():
print(f"❌ Error starting exam: {str(e)}") print(f"❌ Error starting exam: {str(e)}")
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
# ✅ MISSING ROUTE - This was causing the 404 error!
@bp.route('/submit-solution', methods=['POST', 'OPTIONS'])
def submit_solution():
"""Submit coding solution for evaluation"""
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.get_json()
exam_code = data.get('exam_code')
username = data.get('username')
problem_id = data.get('problem_id', 'problem_1')
code = data.get('code')
language = data.get('language', 'python')
if not all([exam_code, username, code]):
return jsonify({
"success": False,
"error": "Missing required fields: exam_code, username, code"
}), 400
print(f"📝 Solution submission: {username} -> {exam_code} (Problem: {problem_id})")
# Find the exam
exam = db.exams.find_one({"exam_code": exam_code.upper()})
if not exam:
return jsonify({"success": False, "error": "Exam not found"}), 404
# Find the specific problem (support both old and new format)
problem = None
if exam.get('problems'):
problem = next((p for p in exam.get('problems', []) if p.get('id') == problem_id), None)
if not problem and exam.get('problem'):
problem = exam['problem']
problem['id'] = 'problem_1'
if not problem:
return jsonify({"success": False, "error": "Problem not found"}), 404
# Use the enhanced dynamic scoring system from main.py
try:
from main import calculate_dynamic_score
result = calculate_dynamic_score(code, language, problem)
except ImportError:
# Fallback basic scoring if main function not available
result = {
'score': 50, # Default score
'passed_tests': 1,
'total_tests': 1,
'test_results': [{'passed': True, 'description': 'Basic test'}],
'execution_time': 0.1,
'details': {'points_earned': 50, 'total_points': 100}
}
# Create submission record
submission = {
"submission_id": str(uuid.uuid4()),
"exam_code": exam_code.upper(),
"username": username,
"problem_id": problem_id,
"code": code,
"language": language,
"score": result['score'],
"passed_tests": result['passed_tests'],
"total_tests": result['total_tests'],
"test_results": result['test_results'],
"execution_time": result['execution_time'],
"submitted_at": datetime.now(),
"points_earned": result['details']['points_earned'],
"total_points": result['details']['total_points']
}
# Save submission to submissions collection
db.submissions.insert_one(submission)
# Update participant in exam
participant_update = {
"score": result['score'],
"completed": True,
"submission_time": datetime.now(),
"language": language,
"submission": code,
"test_results": result['test_results']
}
exam_update_result = db.exams.update_one(
{"exam_code": exam_code.upper(), "participants.name": username},
{"$set": {f"participants.$": {**participant_update, "name": username, "joined_at": datetime.now(), "session_id": str(uuid.uuid4())}}}
)
# Update participant leaderboard in separate collection
participant_filter = {"exam_code": exam_code.upper(), "username": username}
participant = db.participants.find_one(participant_filter)
if participant:
# Update existing participant
total_score = participant.get('total_score', 0) + result['details']['points_earned']
problems_solved = participant.get('problems_solved', 0)
if result['score'] == 100: # Perfect score
problems_solved += 1
db.participants.update_one(
participant_filter,
{
"$set": {
"total_score": total_score,
"problems_solved": problems_solved,
"last_submission": datetime.now()
},
"$push": {
"submissions": {
"problem_id": problem_id,
"score": result['score'],
"points": result['details']['points_earned'],
"submitted_at": datetime.now()
}
}
}
)
else:
# Create new participant
new_participant = {
"exam_code": exam_code.upper(),
"username": username,
"total_score": result['details']['points_earned'],
"problems_solved": 1 if result['score'] == 100 else 0,
"joined_at": datetime.now(),
"last_submission": datetime.now(),
"submissions": [{
"problem_id": problem_id,
"score": result['score'],
"points": result['details']['points_earned'],
"submitted_at": datetime.now()
}]
}
db.participants.insert_one(new_participant)
print(f"✅ Solution submitted: {result['score']}% ({result['passed_tests']}/{result['total_tests']} tests)")
return jsonify({
"success": True,
"message": f"Solution submitted successfully! Score: {result['score']}%",
"result": {
"score": result['score'],
"passed_tests": result['passed_tests'],
"total_tests": result['total_tests'],
"test_results": result['test_results'],
"execution_time": result['execution_time'],
"points_earned": result['details']['points_earned'],
"total_points": result['details']['total_points']
}
})
except Exception as e:
print(f"❌ Submission error: {str(e)}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@bp.route("/leaderboard/<exam_code>", methods=["GET", "OPTIONS"]) @bp.route("/leaderboard/<exam_code>", methods=["GET", "OPTIONS"])
def get_leaderboard(exam_code): def get_leaderboard(exam_code):
"""Get real-time leaderboard visible to all participants""" """Get real-time leaderboard visible to all participants"""
@@ -458,7 +654,7 @@ def get_host_dashboard(exam_code):
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
# ✅ CORRECTED: Host panel management endpoints (using Blueprint decorators) # ✅ FIXED: Remove duplicate definition
@bp.route('/info/<exam_code>', methods=['GET', 'OPTIONS']) @bp.route('/info/<exam_code>', methods=['GET', 'OPTIONS'])
def get_exam_info(exam_code): def get_exam_info(exam_code):
"""Get detailed information about an exam for the host panel""" """Get detailed information about an exam for the host panel"""
@@ -470,26 +666,37 @@ def get_exam_info(exam_code):
return response return response
try: try:
print(f"📊 Host panel requesting info for exam: {exam_code}")
exam = db.exams.find_one({"exam_code": exam_code.upper()}) exam = db.exams.find_one({"exam_code": exam_code.upper()})
if not exam: if not exam:
print(f"❌ Exam not found: {exam_code}")
return jsonify({"success": False, "error": "Exam not found"}), 404 return jsonify({"success": False, "error": "Exam not found"}), 404
# Convert datetime objects to strings for JSON serialization
created_at = exam.get("created_at")
if hasattr(created_at, 'isoformat'):
created_at = created_at.isoformat()
exam_info = { exam_info = {
"title": exam["title"], "title": exam["title"],
"status": exam["status"], "status": exam["status"],
"duration_minutes": exam["duration_minutes"], "duration_minutes": exam["duration_minutes"],
"participants_count": len(exam.get("participants", [])), "participants_count": len(exam.get("participants", [])),
"max_participants": exam["max_participants"], "max_participants": exam.get("max_participants", 50),
"problem_title": exam.get("problem", {}).get("title", exam["title"]), "problem_title": exam.get("problem", {}).get("title", exam["title"]),
"languages": exam.get("problem", {}).get("languages", ["python"]), "languages": exam.get("problem", {}).get("languages", ["python"]),
"created_at": exam["created_at"], "created_at": created_at,
"host_name": exam["host_name"] "host_name": exam["host_name"]
} }
print(f"📊 Host panel requested info for exam {exam_code}") print(f"✅ Found exam: {exam['title']} (Status: {exam['status']})")
return jsonify({"success": True, "exam_info": exam_info}) return jsonify({"success": True, "exam_info": exam_info})
except Exception as e: except Exception as e:
print(f"❌ Error getting exam info: {str(e)}") print(f"❌ Error getting exam info: {str(e)}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500 return jsonify({"success": False, "error": str(e)}), 500
@bp.route('/participants/<exam_code>', methods=['GET', 'OPTIONS']) @bp.route('/participants/<exam_code>', methods=['GET', 'OPTIONS'])
@@ -598,198 +805,7 @@ def stop_exam():
print(f"❌ Error stopping exam: {str(e)}") print(f"❌ Error stopping exam: {str(e)}")
return jsonify({"success": False, "error": str(e)}), 500 return jsonify({"success": False, "error": str(e)}), 500
@bp.route("/debug-join-data", methods=["POST", "OPTIONS"]) # ✅ FIXED: Remove duplicate definition
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/info/<exam_code>",
"/api/exam/participants/<exam_code>",
"/api/exam/remove-participant",
"/api/exam/stop-exam",
"/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/info/<exam_code>",
"/api/exam/participants/<exam_code>",
"/api/exam/remove-participant",
"/api/exam/stop-exam",
"/api/exam/test",
"/api/exam/debug-join-data"
]
})
@bp.route('/info/<exam_code>', methods=['GET', 'OPTIONS'])
def get_exam_info(exam_code):
"""Get detailed information about an exam for the host panel"""
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"📊 Host panel requesting info for exam: {exam_code}")
exam = db.exams.find_one({"exam_code": exam_code.upper()})
if not exam:
print(f"❌ Exam not found: {exam_code}")
return jsonify({"success": False, "error": "Exam not found"}), 404
# Convert datetime objects to strings for JSON serialization
created_at = exam.get("created_at")
if hasattr(created_at, 'isoformat'):
created_at = created_at.isoformat()
exam_info = {
"title": exam["title"],
"status": exam["status"],
"duration_minutes": exam["duration_minutes"],
"participants_count": len(exam.get("participants", [])),
"max_participants": exam.get("max_participants", 50),
"problem_title": exam.get("problem", {}).get("title", exam["title"]),
"languages": exam.get("problem", {}).get("languages", ["python"]),
"created_at": created_at,
"host_name": exam["host_name"]
}
print(f"✅ Found exam: {exam['title']} (Status: {exam['status']})")
return jsonify({"success": True, "exam_info": exam_info})
except Exception as e:
print(f"❌ Error getting exam info: {str(e)}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@bp.route('/upload-question', methods=['POST', 'OPTIONS'])
def upload_question():
"""Host uploads a custom question to their 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.get_json()
exam_code = data.get('exam_code', '').upper()
question_data = data.get('question', {})
print(f"📤 Host uploading question to exam: {exam_code}")
if not exam_code or not question_data:
return jsonify({"success": False, "error": "Missing exam_code or question data"}), 400
# Validate required question fields
required_fields = ['title', 'description', 'function_name', 'test_cases']
for field in required_fields:
if not question_data.get(field):
return jsonify({"success": False, "error": f"Missing required field: {field}"}), 400
# Find the exam
exam = db.exams.find_one({"exam_code": exam_code})
if not exam:
return jsonify({"success": False, "error": "Exam not found"}), 404
# Check if exam has already started
if exam['status'] != 'waiting':
return jsonify({"success": False, "error": "Cannot modify questions after exam has started"}), 400
# Generate question ID
question_id = str(uuid.uuid4())
# Prepare question document
question = {
"id": question_id,
"title": question_data['title'],
"description": question_data['description'],
"difficulty": question_data.get('difficulty', 'medium'),
"function_name": question_data['function_name'],
"starter_code": question_data.get('starter_code', {
'python': f'def {question_data["function_name"]}():\n # Write your solution here\n pass'
}),
"test_cases": question_data['test_cases'],
"examples": question_data.get('examples', []),
"constraints": question_data.get('constraints', []),
"time_limit": question_data.get('time_limit', 1000),
"memory_limit": question_data.get('memory_limit', '128MB'),
"created_at": datetime.now(),
"uploaded_by": exam['host_name']
}
# Update the exam with the new question
result = db.exams.update_one(
{"exam_code": exam_code},
{
"$set": {
"problem": question,
"updated_at": datetime.now()
}
}
)
if result.modified_count > 0:
print(f"✅ Question '{question['title']}' uploaded to exam {exam_code}")
return jsonify({
"success": True,
"message": "Question uploaded successfully",
"question_id": question_id,
"question_title": question['title']
})
else:
return jsonify({"success": False, "error": "Failed to update exam"}), 500
except Exception as e:
print(f"❌ Error uploading question: {str(e)}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": f"Server error: {str(e)}"}), 500
@bp.route('/upload-question', methods=['POST', 'OPTIONS']) @bp.route('/upload-question', methods=['POST', 'OPTIONS'])
def upload_question(): def upload_question():
"""Host uploads a custom question to their exam""" """Host uploads a custom question to their exam"""
@@ -826,7 +842,6 @@ def upload_question():
return jsonify({"success": False, "error": "Cannot modify questions after exam has started"}), 400 return jsonify({"success": False, "error": "Cannot modify questions after exam has started"}), 400
# Generate question ID # Generate question ID
import uuid
question_id = str(uuid.uuid4()) question_id = str(uuid.uuid4())
# Prepare question document # Prepare question document
@@ -845,7 +860,9 @@ def upload_question():
"time_limit": question_data.get('time_limit', 1000), "time_limit": question_data.get('time_limit', 1000),
"memory_limit": question_data.get('memory_limit', '128MB'), "memory_limit": question_data.get('memory_limit', '128MB'),
"created_at": datetime.now(), "created_at": datetime.now(),
"uploaded_by": exam.get('host_name', 'Unknown') "uploaded_by": exam.get('host_name', 'Unknown'),
"languages": question_data.get('languages', ['python']),
"total_points": question_data.get('total_points', 100)
} }
# Update the exam with the new question # Update the exam with the new question
@@ -929,3 +946,73 @@ def update_duration():
except Exception as e: except Exception as e:
print(f"❌ Error updating duration: {str(e)}") print(f"❌ Error updating duration: {str(e)}")
return jsonify({"success": False, "error": str(e)}), 500 return jsonify({"success": False, "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/submit-solution", # ✅ Now included!
"/api/exam/leaderboard/<exam_code>",
"/api/exam/get-problem/<exam_code>",
"/api/exam/host-dashboard/<exam_code>",
"/api/exam/info/<exam_code>",
"/api/exam/participants/<exam_code>",
"/api/exam/remove-participant",
"/api/exam/stop-exam",
"/api/exam/upload-question",
"/api/exam/update-duration",
"/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/submit-solution", # ✅ Now included!
"/api/exam/leaderboard/<exam_code>",
"/api/exam/get-problem/<exam_code>",
"/api/exam/host-dashboard/<exam_code>",
"/api/exam/info/<exam_code>",
"/api/exam/participants/<exam_code>",
"/api/exam/remove-participant",
"/api/exam/stop-exam",
"/api/exam/upload-question",
"/api/exam/update-duration",
"/api/exam/test",
"/api/exam/debug-join-data"
]
})
+1037 -27
View File
File diff suppressed because it is too large Load Diff
+346
View File
@@ -0,0 +1,346 @@
import tensorflow as tf
import pickle
import json
import numpy as np
import random
import os
from tensorflow.keras.preprocessing.sequence import pad_sequences
from datetime import datetime
from bson import ObjectId
class AdaptiveQuizMasterLLM:
def __init__(self, models_path="./models/"):
"""
Intelligent Quiz Master with optional model loading
"""
self.models_path = models_path
self.model_available = False
# Try to load model components
try:
# Check if model files exist
model_file = f'{models_path}improved_cnn_model.h5'
tokenizer_file = f'{models_path}tokenizer.pickle'
label_encoder_file = f'{models_path}label_encoder.pickle'
data_file = f'{models_path}processed_commonsenseqa_data.json'
if all(os.path.exists(f) for f in [model_file, tokenizer_file, label_encoder_file, data_file]):
# Load model with compatibility handling
try:
self.model = tf.keras.models.load_model(model_file)
self.model_available = True
print("✅ CNN Model loaded successfully")
except Exception as model_error:
print(f"⚠️ Model loading failed: {model_error}")
print("🔄 Continuing without AI predictions...")
self.model = None
self.model_available = False
# Load other components
with open(tokenizer_file, 'rb') as f:
self.tokenizer = pickle.load(f)
with open(label_encoder_file, 'rb') as f:
self.label_encoder = pickle.load(f)
with open(data_file, 'r') as f:
self.quiz_data = json.load(f)
else:
print("⚠️ Model files not found. Using fallback quiz data...")
self.model = None
self.tokenizer = None
self.label_encoder = None
self.quiz_data = self._get_fallback_questions()
self.model_available = False
except Exception as e:
print(f"⚠️ Model initialization failed: {e}")
print("🔄 Using fallback mode...")
self.model = None
self.tokenizer = None
self.label_encoder = None
self.quiz_data = self._get_fallback_questions()
self.model_available = False
# Separate questions by difficulty
self.questions_by_difficulty = {
'easy': [q for q in self.quiz_data if q.get('difficulty') == 'easy'],
'medium': [q for q in self.quiz_data if q.get('difficulty') == 'medium'],
'hard': [q for q in self.quiz_data if q.get('difficulty') == 'hard']
}
print("🤖 Adaptive Quiz Master LLM initialized!")
print(f"📊 Model Available: {self.model_available}")
print(f"📊 Questions: Easy({len(self.questions_by_difficulty['easy'])}), Medium({len(self.questions_by_difficulty['medium'])}), Hard({len(self.questions_by_difficulty['hard'])})")
def _get_fallback_questions(self):
"""
Fallback questions when model files are not available
"""
return [
{
"question": "What is the capital of France?",
"incorrect_answers": ["London", "Berlin", "Madrid"],
"correct_answer": "Paris",
"difficulty": "easy"
},
{
"question": "Which programming language is known for its simplicity and readability?",
"incorrect_answers": ["C++", "Assembly", "Java"],
"correct_answer": "Python",
"difficulty": "easy"
},
{
"question": "What does API stand for?",
"incorrect_answers": ["Advanced Programming Interface", "Automated Program Integration", "Applied Programming Instructions"],
"correct_answer": "Application Programming Interface",
"difficulty": "medium"
},
{
"question": "In machine learning, what does 'overfitting' mean?",
"incorrect_answers": ["Model performs well on all data", "Model is too simple", "Model trains too quickly"],
"correct_answer": "Model memorizes training data but fails on new data",
"difficulty": "medium"
},
{
"question": "What is the time complexity of binary search?",
"incorrect_answers": ["O(n)", "O(n²)", "O(n log n)"],
"correct_answer": "O(log n)",
"difficulty": "hard"
},
{
"question": "Which design pattern ensures a class has only one instance?",
"incorrect_answers": ["Factory", "Observer", "Strategy"],
"correct_answer": "Singleton",
"difficulty": "hard"
}
]
def create_session(self, user_id):
"""
Create new adaptive quiz session
"""
session_id = str(ObjectId())
session_data = {
'session_id': session_id,
'user_id': user_id,
'current_difficulty': 'easy', # Always start with easy
'consecutive_correct': {'easy': 0, 'medium': 0, 'hard': 0},
'total_questions': 0,
'total_correct': 0,
'question_history': [],
'created_at': datetime.utcnow(),
'status': 'active'
}
return session_data
def get_adaptive_question(self, session_data):
"""
Get next question based on current difficulty level
"""
current_difficulty = session_data['current_difficulty']
available_questions = self.questions_by_difficulty[current_difficulty]
# Avoid repeating questions
asked_questions = [q['question_id'] for q in session_data.get('question_history', [])]
available_questions = [q for q in available_questions
if q.get('id', str(hash(q['question']))) not in asked_questions]
if not available_questions:
# Fallback to any difficulty if current level exhausted
all_available = [q for q in self.quiz_data
if q.get('id', str(hash(q['question']))) not in asked_questions]
available_questions = all_available[:10] if all_available else self.quiz_data[:5]
# Select random question
question_data = random.choice(available_questions)
# Create formatted question with shuffled choices
choices = question_data['incorrect_answers'] + [question_data['correct_answer']]
random.shuffle(choices)
# Find correct answer position
correct_position = choices.index(question_data['correct_answer'])
correct_letter = chr(65 + correct_position)
question_obj = {
'question_id': question_data.get('id', str(hash(question_data['question']))),
'question_text': question_data['question'],
'choices': {
'A': choices[0],
'B': choices[1],
'C': choices[2],
'D': choices[3]
},
'correct_answer': correct_letter,
'difficulty': current_difficulty,
'explanation': f"The correct answer is {question_data['correct_answer']}."
}
return question_obj
def get_llm_prediction(self, question_text, choices):
"""
Use trained model to predict answer (with fallback)
"""
if not self.model_available or not self.model:
# Fallback: Random prediction with low confidence
import random
fallback_prediction = random.choice(['A', 'B', 'C', 'D'])
return {
'llm_prediction': fallback_prediction,
'confidence': 0.25, # Random confidence
'model_accuracy': 25.0, # Random accuracy
'fallback_mode': True
}
try:
# Format question for model prediction
formatted_question = f"Difficulty: medium\nQuestion: {question_text}\n"
formatted_question += f"A) {choices['A']}\n"
formatted_question += f"B) {choices['B']}\n"
formatted_question += f"C) {choices['C']}\n"
formatted_question += f"D) {choices['D']}\n"
# Tokenize and predict using your trained model
sequence = self.tokenizer.texts_to_sequences([formatted_question])
padded = pad_sequences(sequence, maxlen=400, padding='post')
prediction = self.model.predict(padded, verbose=0)
predicted_class = np.argmax(prediction[0])
predicted_letter = self.label_encoder.inverse_transform([predicted_class])[0]
confidence = float(prediction[0][predicted_class])
return {
'llm_prediction': predicted_letter,
'confidence': confidence,
'model_accuracy': 33.1, # Your model's test accuracy
'fallback_mode': False
}
except Exception as e:
print(f"⚠️ Prediction error: {e}")
# Fallback on error
import random
fallback_prediction = random.choice(['A', 'B', 'C', 'D'])
return {
'llm_prediction': fallback_prediction,
'confidence': 0.25,
'model_accuracy': 25.0,
'fallback_mode': True,
'error': str(e)
}
def evaluate_answer(self, session_data, question_data, user_answer):
"""
Evaluate user answer and adjust difficulty according to your rules
"""
is_correct = (user_answer.upper() == question_data['correct_answer'])
current_difficulty = session_data['current_difficulty']
# Update session stats
session_data['total_questions'] += 1
if is_correct:
session_data['total_correct'] += 1
session_data['consecutive_correct'][current_difficulty] += 1
else:
# Reset consecutive count for current difficulty
session_data['consecutive_correct'][current_difficulty] = 0
# Apply your exact difficulty adjustment rules
new_difficulty = self._adjust_difficulty(session_data, is_correct)
# Record question in history
question_record = {
'question_id': question_data['question_id'],
'question_text': question_data['question_text'],
'user_answer': user_answer,
'correct_answer': question_data['correct_answer'],
'is_correct': is_correct,
'difficulty': current_difficulty,
'timestamp': datetime.utcnow()
}
session_data['question_history'].append(question_record)
# Get LLM prediction for comparison
llm_result = self.get_llm_prediction(question_data['question_text'], question_data['choices'])
result = {
'is_correct': is_correct,
'correct_answer': question_data['correct_answer'],
'explanation': question_data['explanation'],
'difficulty_changed': new_difficulty != current_difficulty,
'previous_difficulty': current_difficulty,
'new_difficulty': new_difficulty,
'consecutive_correct': session_data['consecutive_correct'][current_difficulty],
'llm_prediction': llm_result,
'session_stats': {
'total_questions': session_data['total_questions'],
'total_correct': session_data['total_correct'],
'accuracy': round((session_data['total_correct'] / session_data['total_questions']) * 100, 1)
}
}
session_data['current_difficulty'] = new_difficulty
return result
def _adjust_difficulty(self, session_data, is_correct):
"""
Your exact difficulty adjustment rules:
- 3 consecutive correct: EasyMediumHard
- 1 incorrect: HardMediumEasy (stay on Easy if already there)
"""
current_difficulty = session_data['current_difficulty']
consecutive = session_data['consecutive_correct']
if is_correct:
# Move up after 3 consecutive correct answers
if consecutive[current_difficulty] >= 3:
if current_difficulty == 'easy':
# Reset consecutive count for easy, start fresh for medium
session_data['consecutive_correct']['easy'] = 0
return 'medium'
elif current_difficulty == 'medium':
# Reset consecutive count for medium, start fresh for hard
session_data['consecutive_correct']['medium'] = 0
return 'hard'
# If already hard, stay hard
else:
# Move down immediately after 1 wrong answer
if current_difficulty == 'hard':
return 'medium'
elif current_difficulty == 'medium':
return 'easy'
# If already easy, stay easy
return current_difficulty
def get_session_stats(self, session_data):
"""
Get comprehensive session statistics
"""
total_questions = session_data['total_questions']
total_correct = session_data['total_correct']
accuracy = (total_correct / total_questions * 100) if total_questions > 0 else 0
difficulty_stats = {}
for difficulty in ['easy', 'medium', 'hard']:
questions_at_level = [q for q in session_data['question_history'] if q['difficulty'] == difficulty]
correct_at_level = sum(1 for q in questions_at_level if q['is_correct'])
difficulty_stats[difficulty] = {
'questions': len(questions_at_level),
'correct': correct_at_level,
'accuracy': round((correct_at_level / len(questions_at_level) * 100), 1) if questions_at_level else 0
}
return {
'session_id': session_data['session_id'],
'current_difficulty': session_data['current_difficulty'],
'total_questions': total_questions,
'total_correct': total_correct,
'overall_accuracy': round(accuracy, 1),
'consecutive_correct': session_data['consecutive_correct'],
'difficulty_breakdown': difficulty_stats,
'status': session_data['status']
}
+647
View File
@@ -0,0 +1,647 @@
import tensorflow as tf
import pickle
import json
import numpy as np
import random
import os
from tensorflow.keras.preprocessing.sequence import pad_sequences
from datetime import datetime
from bson import ObjectId
import uuid
class AdaptiveQuizMasterLLM:
def __init__(self, models_path="./models/"):
"""
Intelligent Quiz Master with enhanced fallback questions and AI generation
"""
self.models_path = models_path
self.model_available = False
try:
# Try to load model files
model_file = f'{models_path}improved_cnn_model.h5'
tokenizer_file = f'{models_path}tokenizer.pickle'
label_encoder_file = f'{models_path}label_encoder.pickle'
data_file = f'{models_path}processed_commonsenseqa_data.json'
if all(os.path.exists(f) for f in [model_file, tokenizer_file, label_encoder_file, data_file]):
try:
self.model = tf.keras.models.load_model(model_file)
print("✅ CNN Model loaded successfully")
self.model_available = True
except Exception as e:
print(f"⚠️ Model loading failed: {e}")
self.model = None
self.model_available = False
with open(tokenizer_file, 'rb') as f:
self.tokenizer = pickle.load(f)
with open(label_encoder_file, 'rb') as f:
self.label_encoder = pickle.load(f)
with open(data_file, 'r') as f:
self.quiz_data = json.load(f)
else:
print("⚠️ Model files not found. Using enhanced fallback questions...")
self.model = None
self.tokenizer = None
self.label_encoder = None
self.quiz_data = self._get_enhanced_fallback_questions()
self.model_available = False
except Exception as e:
print(f"⚠️ Model initialization failed: {e}")
self.model = None
self.tokenizer = None
self.label_encoder = None
self.quiz_data = self._get_enhanced_fallback_questions()
self.model_available = False
# Distribute questions by difficulty
self.questions_by_difficulty = {
'easy': [q for q in self.quiz_data if q.get('difficulty') == 'easy'],
'medium': [q for q in self.quiz_data if q.get('difficulty') == 'medium'],
'hard': [q for q in self.quiz_data if q.get('difficulty') == 'hard']
}
# If no questions are categorized, distribute fallback questions
if not any(self.questions_by_difficulty.values()):
self._distribute_fallback_questions()
print("🤖 AdaptiveQuizMasterLLM initialized")
print(f"📊 Model Available: {self.model_available}")
print(f"📊 Questions: Easy({len(self.questions_by_difficulty['easy'])}), "
f"Medium({len(self.questions_by_difficulty['medium'])}), "
f"Hard({len(self.questions_by_difficulty['hard'])})")
def _get_enhanced_fallback_questions(self):
"""Enhanced fallback questions with comprehensive coverage"""
return [
# ===== EASY QUESTIONS =====
{
"id": "easy_1",
"question": "What is the capital of France?",
"incorrect_answers": ["London", "Berlin", "Madrid"],
"correct_answer": "Paris",
"difficulty": "easy",
"category": "Geography"
},
{
"id": "easy_2",
"question": "Which programming language is known for its simplicity and readability?",
"incorrect_answers": ["C++", "Assembly", "Java"],
"correct_answer": "Python",
"difficulty": "easy",
"category": "Programming"
},
{
"id": "easy_3",
"question": "What does HTML stand for?",
"incorrect_answers": ["High Tech Modern Language", "Home Tool Markup Language", "Hyperlink Text Language"],
"correct_answer": "HyperText Markup Language",
"difficulty": "easy",
"category": "Web Development"
},
{
"id": "easy_4",
"question": "Which of these is a web browser?",
"incorrect_answers": ["Microsoft Word", "Adobe Photoshop", "Spotify"],
"correct_answer": "Google Chrome",
"difficulty": "easy",
"category": "Technology"
},
{
"id": "easy_5",
"question": "What is 2 + 2?",
"incorrect_answers": ["3", "5", "6"],
"correct_answer": "4",
"difficulty": "easy",
"category": "Mathematics"
},
{
"id": "easy_6",
"question": "Which planet is closest to the Sun?",
"incorrect_answers": ["Venus", "Earth", "Mars"],
"correct_answer": "Mercury",
"difficulty": "easy",
"category": "Science"
},
{
"id": "easy_7",
"question": "What does CSS stand for?",
"incorrect_answers": ["Computer Style Sheets", "Creative Style Sheets", "Colorful Style Sheets"],
"correct_answer": "Cascading Style Sheets",
"difficulty": "easy",
"category": "Web Development"
},
{
"id": "easy_8",
"question": "Which company developed the iPhone?",
"incorrect_answers": ["Google", "Microsoft", "Samsung"],
"correct_answer": "Apple",
"difficulty": "easy",
"category": "Technology"
},
{
"id": "easy_9",
"question": "What is the largest ocean on Earth?",
"incorrect_answers": ["Atlantic", "Indian", "Arctic"],
"correct_answer": "Pacific",
"difficulty": "easy",
"category": "Geography"
},
{
"id": "easy_10",
"question": "Which data type stores whole numbers in programming?",
"incorrect_answers": ["float", "string", "boolean"],
"correct_answer": "integer",
"difficulty": "easy",
"category": "Programming"
},
# ===== MEDIUM QUESTIONS =====
{
"id": "medium_1",
"question": "What does API stand for?",
"incorrect_answers": ["Advanced Programming Interface", "Automated Program Integration", "Applied Programming Instructions"],
"correct_answer": "Application Programming Interface",
"difficulty": "medium",
"category": "Programming"
},
{
"id": "medium_2",
"question": "In machine learning, what does 'overfitting' mean?",
"incorrect_answers": ["Model performs well on all data", "Model is too simple", "Model trains too quickly"],
"correct_answer": "Model memorizes training data but fails on new data",
"difficulty": "medium",
"category": "Machine Learning"
},
{
"id": "medium_3",
"question": "Which HTTP status code indicates 'Not Found'?",
"incorrect_answers": ["200", "500", "403"],
"correct_answer": "404",
"difficulty": "medium",
"category": "Web Development"
},
{
"id": "medium_4",
"question": "What is the primary purpose of a database index?",
"incorrect_answers": ["Store data", "Backup data", "Encrypt data"],
"correct_answer": "Speed up data retrieval",
"difficulty": "medium",
"category": "Database"
},
{
"id": "medium_5",
"question": "In React, what is a component?",
"incorrect_answers": ["A CSS framework", "A database table", "A server endpoint"],
"correct_answer": "A reusable piece of UI",
"difficulty": "medium",
"category": "React"
},
{
"id": "medium_6",
"question": "What does CPU stand for?",
"incorrect_answers": ["Computer Programming Unit", "Central Program Unit", "Control Program Utility"],
"correct_answer": "Central Processing Unit",
"difficulty": "medium",
"category": "Hardware"
},
{
"id": "medium_7",
"question": "Which sorting algorithm has the best average time complexity?",
"incorrect_answers": ["Bubble Sort", "Selection Sort", "Insertion Sort"],
"correct_answer": "Quick Sort",
"difficulty": "medium",
"category": "Algorithms"
},
{
"id": "medium_8",
"question": "What is the difference between '==' and '===' in JavaScript?",
"incorrect_answers": ["No difference", "=== is for strings only", "== is deprecated"],
"correct_answer": "=== checks type and value, == only checks value",
"difficulty": "medium",
"category": "JavaScript"
},
{
"id": "medium_9",
"question": "In SQL, what does JOIN do?",
"incorrect_answers": ["Creates a new table", "Deletes records", "Updates data"],
"correct_answer": "Combines rows from multiple tables",
"difficulty": "medium",
"category": "Database"
},
{
"id": "medium_10",
"question": "What is the purpose of version control systems like Git?",
"incorrect_answers": ["Code compilation", "Database management", "User interface design"],
"correct_answer": "Track changes in source code",
"difficulty": "medium",
"category": "Development Tools"
},
# ===== HARD QUESTIONS =====
{
"id": "hard_1",
"question": "What is the time complexity of binary search?",
"incorrect_answers": ["O(n)", "O(n²)", "O(n log n)"],
"correct_answer": "O(log n)",
"difficulty": "hard",
"category": "Algorithms"
},
{
"id": "hard_2",
"question": "Which design pattern ensures a class has only one instance?",
"incorrect_answers": ["Factory", "Observer", "Strategy"],
"correct_answer": "Singleton",
"difficulty": "hard",
"category": "Design Patterns"
},
{
"id": "hard_3",
"question": "In distributed systems, what is the CAP theorem?",
"incorrect_answers": ["Consistency, Availability, Performance", "Concurrency, Atomicity, Persistence", "Caching, Authentication, Privacy"],
"correct_answer": "Consistency, Availability, Partition tolerance",
"difficulty": "hard",
"category": "Distributed Systems"
},
{
"id": "hard_4",
"question": "What is the space complexity of merge sort?",
"incorrect_answers": ["O(1)", "O(log n)", "O(n²)"],
"correct_answer": "O(n)",
"difficulty": "hard",
"category": "Algorithms"
},
{
"id": "hard_5",
"question": "In functional programming, what is a closure?",
"incorrect_answers": ["A loop structure", "A data type", "A compilation step"],
"correct_answer": "A function that captures variables from its scope",
"difficulty": "hard",
"category": "Programming Concepts"
},
{
"id": "hard_6",
"question": "What is the purpose of hash table collision resolution?",
"incorrect_answers": ["Increase memory usage", "Slow down operations", "Reduce security"],
"correct_answer": "Handle multiple keys mapping to the same slot",
"difficulty": "hard",
"category": "Data Structures"
},
{
"id": "hard_7",
"question": "In microservices architecture, what is service discovery?",
"incorrect_answers": ["Database replication", "Load balancing", "Code deployment"],
"correct_answer": "Mechanism for services to find and communicate with each other",
"difficulty": "hard",
"category": "Architecture"
},
{
"id": "hard_8",
"question": "What is the difference between TCP and UDP?",
"incorrect_answers": ["UDP is faster but unreliable", "TCP is for web only", "No significant difference"],
"correct_answer": "TCP is reliable and connection-oriented, UDP is fast but unreliable",
"difficulty": "hard",
"category": "Networking"
},
{
"id": "hard_9",
"question": "In machine learning, what is the curse of dimensionality?",
"incorrect_answers": ["Too much training data", "Overly complex models", "Hardware limitations"],
"correct_answer": "Performance degradation as feature dimensions increase",
"difficulty": "hard",
"category": "Machine Learning"
},
{
"id": "hard_10",
"question": "What is eventual consistency in distributed databases?",
"incorrect_answers": ["Data is always consistent", "Consistency is never achieved", "Only one node has data"],
"correct_answer": "System will become consistent over time without continuous input",
"difficulty": "hard",
"category": "Distributed Systems"
}
]
def _distribute_fallback_questions(self):
"""Distribute fallback questions into difficulty levels"""
fallback_data = self._get_enhanced_fallback_questions()
self.quiz_data = fallback_data
self.questions_by_difficulty = {
'easy': [q for q in fallback_data if q.get('difficulty') == 'easy'],
'medium': [q for q in fallback_data if q.get('difficulty') == 'medium'],
'hard': [q for q in fallback_data if q.get('difficulty') == 'hard']
}
def generate_quiz(self, topic=None, difficulty=None, num_questions=5):
"""
Generate a quiz compatible with room-based quiz system - FIXED VERSION
"""
print(f"🤖 Generating quiz: topic={topic}, difficulty={difficulty}, num_questions={num_questions}")
# Filter questions based on topic and difficulty
filtered = self.quiz_data.copy()
if topic and topic.lower() != 'general':
filtered = [q for q in filtered if
topic.lower() in q.get('question', '').lower() or
topic.lower() in q.get('category', '').lower()]
print(f"📝 Filtered by topic '{topic}': {len(filtered)} questions")
if difficulty:
filtered = [q for q in filtered if q.get('difficulty', 'medium') == difficulty]
print(f"📝 Filtered by difficulty '{difficulty}': {len(filtered)} questions")
# Ensure we have questions to select from
if not filtered:
print("⚠️ No questions match criteria, using all available questions")
filtered = self.quiz_data[:10] # Use first 10 as fallback
# Select random questions
selected = random.sample(filtered, min(num_questions, len(filtered)))
print(f"📝 Selected {len(selected)} questions from {len(filtered)} filtered questions")
questions = []
for i, q_data in enumerate(selected):
choices = q_data['incorrect_answers'] + [q_data['correct_answer']]
random.shuffle(choices)
correct_idx = choices.index(q_data['correct_answer'])
questions.append({
"id": str(uuid.uuid4()),
"question_number": i + 1,
"question_text": q_data['question'],
"options": choices,
"correct_answer": chr(65 + correct_idx), # A, B, C, D
"points": 10 if q_data.get('difficulty') == 'easy' else 15 if q_data.get('difficulty') == 'medium' else 20,
"explanation": f"The correct answer is {q_data['correct_answer']}.",
"difficulty": q_data.get('difficulty', 'medium'),
"category": q_data.get('category', 'General')
})
quiz_result = {
"id": str(uuid.uuid4()),
"title": f"AI Generated Quiz{(' - ' + topic) if topic and topic.lower() != 'general' else ''}",
"description": f"Quiz generated by AI. Topic: {topic or 'General'}, Difficulty: {difficulty or 'Mixed'}",
"difficulty": difficulty or "mixed",
"questions": questions,
"created_at": datetime.now().isoformat(),
"generated_by": "AI",
"total_points": sum(q['points'] for q in questions)
}
print(f"✅ Quiz generated successfully: {len(questions)} questions, {quiz_result['total_points']} total points")
return quiz_result
def create_session(self, user_id):
"""Create new adaptive quiz session"""
session_id = str(ObjectId())
session_data = {
'session_id': session_id,
'user_id': user_id,
'current_difficulty': 'easy', # Always start with easy
'consecutive_correct': {'easy': 0, 'medium': 0, 'hard': 0},
'total_questions': 0,
'total_correct': 0,
'question_history': [],
'created_at': datetime.utcnow(),
'status': 'active'
}
return session_data
def get_adaptive_question(self, session_data):
"""Get next question based on current difficulty level"""
current_difficulty = session_data['current_difficulty']
available_questions = self.questions_by_difficulty[current_difficulty].copy()
# Avoid repeating questions
asked_questions = [q['question_id'] for q in session_data.get('question_history', [])]
available_questions = [q for q in available_questions
if q.get('id', str(hash(q['question']))) not in asked_questions]
if not available_questions:
# Fallback to any difficulty if current level exhausted
all_available = [q for q in self.quiz_data
if q.get('id', str(hash(q['question']))) not in asked_questions]
available_questions = all_available[:10] if all_available else self.quiz_data[:5]
# Select random question
question_data = random.choice(available_questions)
# Create formatted question with shuffled choices
choices = question_data['incorrect_answers'] + [question_data['correct_answer']]
random.shuffle(choices)
# Find correct answer position
correct_position = choices.index(question_data['correct_answer'])
correct_letter = chr(65 + correct_position)
question_obj = {
'question_id': question_data.get('id', str(hash(question_data['question']))),
'question_text': question_data['question'],
'choices': {
'A': choices[0],
'B': choices[1],
'C': choices[2],
'D': choices[3]
},
'correct_answer': correct_letter,
'correct_answer_text': question_data['correct_answer'],
'difficulty': current_difficulty,
'category': question_data.get('category', 'General'),
'explanation': f"The correct answer is {question_data['correct_answer']}."
}
return question_obj
def get_llm_prediction(self, question_text, choices):
"""Use trained model to predict answer (with intelligent fallback)"""
if not self.model_available or not self.model:
# Intelligent fallback with pattern matching
question_lower = question_text.lower()
choice_keys = list(choices.keys()) if isinstance(choices, dict) else ['A', 'B', 'C', 'D']
choice_texts = [choices[key].lower() if isinstance(choices, dict) else choices[i].lower()
for i, key in enumerate(choice_keys)]
# Enhanced pattern matching
if 'capital' in question_lower and 'france' in question_lower:
for i, choice in enumerate(choice_texts):
if 'paris' in choice:
return {
'llm_prediction': choice_keys[i],
'confidence': 0.9,
'model_accuracy': 90.0,
'fallback_mode': True,
'reason': 'Pattern matching - France capital'
}
if 'html' in question_lower and 'stand' in question_lower:
for i, choice in enumerate(choice_texts):
if 'hypertext markup' in choice:
return {
'llm_prediction': choice_keys[i],
'confidence': 0.85,
'model_accuracy': 85.0,
'fallback_mode': True,
'reason': 'Pattern matching - HTML definition'
}
# Default random fallback
fallback_prediction = random.choice(choice_keys)
return {
'llm_prediction': fallback_prediction,
'confidence': 0.25,
'model_accuracy': 25.0,
'fallback_mode': True,
'reason': 'Random selection'
}
try:
# Format question for model prediction
formatted_question = f"Question: {question_text}\n"
if isinstance(choices, dict):
for key, choice in choices.items():
formatted_question += f"{key}) {choice}\n"
else:
for i, choice in enumerate(choices):
formatted_question += f"{chr(65+i)}) {choice}\n"
# Tokenize and predict using trained model
sequence = self.tokenizer.texts_to_sequences([formatted_question])
padded = pad_sequences(sequence, maxlen=400, padding='post')
prediction = self.model.predict(padded, verbose=0)
predicted_class = np.argmax(prediction[0])
predicted_letter = self.label_encoder.inverse_transform([predicted_class])[0]
confidence = float(prediction[0][predicted_class])
return {
'llm_prediction': predicted_letter,
'confidence': confidence,
'model_accuracy': 33.1, # Your model's test accuracy
'fallback_mode': False,
'reason': 'CNN model prediction'
}
except Exception as e:
print(f"⚠️ Model prediction error: {e}")
# Fallback on error
fallback_prediction = random.choice(['A', 'B', 'C', 'D'])
return {
'llm_prediction': fallback_prediction,
'confidence': 0.25,
'model_accuracy': 25.0,
'fallback_mode': True,
'error': str(e),
'reason': 'Error fallback'
}
def evaluate_answer(self, session_data, question_data, user_answer):
"""Evaluate user answer and adjust difficulty"""
is_correct = (user_answer.upper() == question_data['correct_answer'])
current_difficulty = session_data['current_difficulty']
# Update session stats
session_data['total_questions'] += 1
if is_correct:
session_data['total_correct'] += 1
session_data['consecutive_correct'][current_difficulty] += 1
else:
# Reset consecutive count for current difficulty
session_data['consecutive_correct'][current_difficulty] = 0
# Apply difficulty adjustment rules
new_difficulty = self._adjust_difficulty(session_data, is_correct)
# Record question in history
question_record = {
'question_id': question_data['question_id'],
'question_text': question_data['question_text'],
'user_answer': user_answer,
'correct_answer': question_data['correct_answer'],
'correct_answer_text': question_data['correct_answer_text'],
'is_correct': is_correct,
'difficulty': current_difficulty,
'category': question_data.get('category', 'General'),
'timestamp': datetime.utcnow()
}
session_data['question_history'].append(question_record)
# Get LLM prediction for comparison
llm_result = self.get_llm_prediction(question_data['question_text'], question_data['choices'])
result = {
'is_correct': is_correct,
'correct_answer': question_data['correct_answer'],
'correct_answer_text': question_data['correct_answer_text'],
'explanation': question_data['explanation'],
'difficulty_changed': new_difficulty != current_difficulty,
'previous_difficulty': current_difficulty,
'new_difficulty': new_difficulty,
'consecutive_correct': session_data['consecutive_correct'][current_difficulty],
'llm_prediction': llm_result,
'llm_agrees': llm_result['llm_prediction'] == question_data['correct_answer'],
'session_stats': {
'total_questions': session_data['total_questions'],
'total_correct': session_data['total_correct'],
'accuracy': round((session_data['total_correct'] / session_data['total_questions']) * 100, 1)
}
}
session_data['current_difficulty'] = new_difficulty
return result
def _adjust_difficulty(self, session_data, is_correct):
"""Difficulty adjustment rules: 3 correct up, 1 wrong down"""
current_difficulty = session_data['current_difficulty']
consecutive = session_data['consecutive_correct']
if is_correct:
# Move up after 3 consecutive correct answers
if consecutive[current_difficulty] >= 3:
if current_difficulty == 'easy':
session_data['consecutive_correct']['easy'] = 0
return 'medium'
elif current_difficulty == 'medium':
session_data['consecutive_correct']['medium'] = 0
return 'hard'
else:
# Move down immediately after 1 wrong answer
if current_difficulty == 'hard':
return 'medium'
elif current_difficulty == 'medium':
return 'easy'
return current_difficulty
def get_session_stats(self, session_data):
"""Get comprehensive session statistics"""
total_questions = session_data['total_questions']
total_correct = session_data['total_correct']
accuracy = (total_correct / total_questions * 100) if total_questions > 0 else 0
difficulty_stats = {}
for difficulty in ['easy', 'medium', 'hard']:
questions_at_level = [q for q in session_data['question_history'] if q['difficulty'] == difficulty]
correct_at_level = sum(1 for q in questions_at_level if q['is_correct'])
difficulty_stats[difficulty] = {
'questions': len(questions_at_level),
'correct': correct_at_level,
'accuracy': round((correct_at_level / len(questions_at_level) * 100), 1) if questions_at_level else 0
}
return {
'session_id': session_data['session_id'],
'current_difficulty': session_data['current_difficulty'],
'total_questions': total_questions,
'total_correct': total_correct,
'overall_accuracy': round(accuracy, 1),
'consecutive_correct': session_data['consecutive_correct'],
'difficulty_breakdown': difficulty_stats,
'status': session_data['status'],
'model_available': self.model_available
}
# Export the class for backward compatibility
AIQuizService = AdaptiveQuizMasterLLM
+449
View File
@@ -0,0 +1,449 @@
'use client'
import React, { useState, useEffect } from 'react'
import { Brain, Target, TrendingUp, Clock, Award, Sparkles, ChevronRight } from 'lucide-react'
interface Question {
question_id: string
question_text: string
choices: {
A: string
B: string
C: string
D: string
}
correct_answer: string
difficulty: string
category: string
}
interface SessionStats {
session_id: string
current_difficulty: string
total_questions: number
total_correct: number
overall_accuracy: number
consecutive_correct: {
easy: number
medium: number
hard: number
}
difficulty_breakdown: {
[key: string]: {
questions: number
correct: number
accuracy: number
}
}
model_available: boolean
}
export default function AdaptiveQuizPage() {
const [sessionId, setSessionId] = useState<string | null>(null)
const [currentQuestion, setCurrentQuestion] = useState<Question | null>(null)
const [selectedAnswer, setSelectedAnswer] = useState<string>('')
const [sessionStats, setSessionStats] = useState<SessionStats | null>(null)
const [loading, setLoading] = useState(false)
const [quizStarted, setQuizStarted] = useState(false)
const [quizCompleted, setQuizCompleted] = useState(false)
const [lastResult, setLastResult] = useState<any>(null)
const [showPrediction, setShowPrediction] = useState(false)
const [aiPrediction, setAIPrediction] = useState<any>(null)
const startQuiz = async () => {
setLoading(true)
try {
const response = await fetch('http://127.0.0.1:5000/api/adaptive-quiz/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
user_id: `user_${Date.now()}`
})
})
const data = await response.json()
if (data.success) {
setSessionId(data.session_id)
setCurrentQuestion(data.question)
setSessionStats(data.session_stats)
setQuizStarted(true)
} else {
alert(`Failed to start quiz: ${data.error}`)
}
} catch (error) {
alert('Network error: Could not start quiz')
} finally {
setLoading(false)
}
}
const submitAnswer = async () => {
if (!selectedAnswer || !currentQuestion || !sessionId) return
setLoading(true)
try {
const response = await fetch(`http://127.0.0.1:5000/api/adaptive-quiz/${sessionId}/answer`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
answer: selectedAnswer,
question_data: currentQuestion
})
})
const data = await response.json()
if (data.success) {
setLastResult(data.result)
if (data.quiz_completed) {
setQuizCompleted(true)
setSessionStats(data.final_stats)
} else {
setCurrentQuestion(data.next_question)
setSessionStats(data.session_stats)
}
setSelectedAnswer('')
setShowPrediction(false)
setAIPrediction(null)
} else {
alert(`Error: ${data.error}`)
}
} catch (error) {
alert('Network error: Could not submit answer')
} finally {
setLoading(false)
}
}
const getAIPrediction = async () => {
if (!currentQuestion) return
try {
const response = await fetch('http://127.0.0.1:5000/api/adaptive-quiz/predict', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
question_text: currentQuestion.question_text,
choices: currentQuestion.choices
})
})
const data = await response.json()
if (data.success) {
setAIPrediction(data.prediction)
setShowPrediction(true)
}
} catch (error) {
console.error('Failed to get AI prediction:', error)
}
}
const getDifficultyColor = (difficulty: string) => {
switch (difficulty) {
case 'easy': return 'text-green-400 bg-green-900'
case 'medium': return 'text-yellow-400 bg-yellow-900'
case 'hard': return 'text-red-400 bg-red-900'
default: return 'text-gray-400 bg-gray-700'
}
}
if (!quizStarted) {
return (
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
<div className="max-w-2xl mx-auto p-6 text-center">
<div className="mb-8">
<Brain className="h-16 w-16 text-purple-400 mx-auto mb-4" />
<h1 className="text-4xl font-bold mb-4">🧠 Adaptive AI Quiz</h1>
<p className="text-gray-400 max-w-lg mx-auto">
Experience an intelligent quiz that adapts to your skill level in real-time using our trained CNN model.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className="bg-gray-800 p-4 rounded-lg">
<Target className="h-8 w-8 text-blue-400 mx-auto mb-2" />
<h3 className="font-semibold mb-1">Adaptive Difficulty</h3>
<p className="text-sm text-gray-400">
Questions adjust based on your performance
</p>
</div>
<div className="bg-gray-800 p-4 rounded-lg">
<Brain className="h-8 w-8 text-purple-400 mx-auto mb-2" />
<h3 className="font-semibold mb-1">AI Predictions</h3>
<p className="text-sm text-gray-400">
See how our AI model would answer
</p>
</div>
<div className="bg-gray-800 p-4 rounded-lg">
<TrendingUp className="h-8 w-8 text-green-400 mx-auto mb-2" />
<h3 className="font-semibold mb-1">Smart Analytics</h3>
<p className="text-sm text-gray-400">
Track performance across difficulty levels
</p>
</div>
</div>
<button
onClick={startQuiz}
disabled={loading}
className="bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 disabled:from-gray-600 disabled:to-gray-600 px-8 py-4 rounded-lg font-semibold flex items-center justify-center space-x-2 mx-auto"
>
{loading ? (
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
) : (
<>
<Sparkles className="h-5 w-5" />
<span>Start Adaptive Quiz</span>
</>
)}
</button>
</div>
</div>
)
}
if (quizCompleted) {
return (
<div className="min-h-screen bg-gray-900 text-white">
<div className="max-w-4xl mx-auto p-6">
<div className="text-center mb-8">
<Award className="h-16 w-16 text-yellow-400 mx-auto mb-4" />
<h1 className="text-3xl font-bold mb-2">Quiz Complete! 🎉</h1>
<p className="text-gray-400">
You've completed the adaptive quiz. Here are your results:
</p>
</div>
{sessionStats && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div className="bg-gray-800 p-6 rounded-lg text-center">
<div className="text-3xl font-bold text-blue-400 mb-2">
{sessionStats.total_questions}
</div>
<div className="text-sm text-gray-400">Total Questions</div>
</div>
<div className="bg-gray-800 p-6 rounded-lg text-center">
<div className="text-3xl font-bold text-green-400 mb-2">
{sessionStats.overall_accuracy}%
</div>
<div className="text-sm text-gray-400">Overall Accuracy</div>
</div>
<div className="bg-gray-800 p-6 rounded-lg text-center">
<div className={`text-3xl font-bold mb-2 ${getDifficultyColor(sessionStats.current_difficulty).split(' ')[0]}`}>
{sessionStats.current_difficulty}
</div>
<div className="text-sm text-gray-400">Final Difficulty</div>
</div>
<div className="bg-gray-800 p-6 rounded-lg text-center">
<div className="text-3xl font-bold text-purple-400 mb-2">
{sessionStats.total_correct}/{sessionStats.total_questions}
</div>
<div className="text-sm text-gray-400">Correct Answers</div>
</div>
</div>
)}
{sessionStats && (
<div className="bg-gray-800 p-6 rounded-lg mb-6">
<h3 className="text-xl font-bold mb-4">Performance by Difficulty</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{Object.entries(sessionStats.difficulty_breakdown).map(([difficulty, stats]) => (
<div key={difficulty} className="bg-gray-900 p-4 rounded">
<div className={`px-2 py-1 rounded text-xs font-medium mb-2 ${getDifficultyColor(difficulty)}`}>
{difficulty.toUpperCase()}
</div>
<div className="text-lg font-bold">{stats.accuracy}%</div>
<div className="text-sm text-gray-400">
{stats.correct}/{stats.questions} questions
</div>
</div>
))}
</div>
</div>
)}
<div className="text-center">
<button
onClick={() => {
setQuizStarted(false)
setQuizCompleted(false)
setSessionId(null)
setCurrentQuestion(null)
setSessionStats(null)
setLastResult(null)
}}
className="bg-blue-600 hover:bg-blue-700 px-8 py-3 rounded-lg font-semibold"
>
Take Another Quiz
</button>
</div>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gray-900 text-white">
<div className="max-w-4xl mx-auto p-6">
{/* Header with Stats */}
{sessionStats && (
<div className="bg-gray-800 p-4 rounded-lg mb-6">
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 text-center">
<div>
<div className="text-lg font-bold text-blue-400">
{sessionStats.total_questions}
</div>
<div className="text-xs text-gray-400">Questions</div>
</div>
<div>
<div className="text-lg font-bold text-green-400">
{sessionStats.overall_accuracy}%
</div>
<div className="text-xs text-gray-400">Accuracy</div>
</div>
<div>
<div className={`text-lg font-bold ${getDifficultyColor(sessionStats.current_difficulty).split(' ')[0]}`}>
{sessionStats.current_difficulty}
</div>
<div className="text-xs text-gray-400">Current Level</div>
</div>
<div>
<div className="text-lg font-bold text-purple-400">
{sessionStats.consecutive_correct[sessionStats.current_difficulty]}
</div>
<div className="text-xs text-gray-400">Streak</div>
</div>
<div>
<div className={`text-sm px-2 py-1 rounded ${sessionStats.model_available ? 'bg-green-900 text-green-400' : 'bg-yellow-900 text-yellow-400'}`}>
{sessionStats.model_available ? '🤖 AI Active' : '🔄 Fallback'}
</div>
</div>
</div>
</div>
)}
{/* Last Result */}
{lastResult && (
<div className={`p-4 rounded-lg mb-6 border-l-4 ${lastResult.is_correct ? 'bg-green-900 border-green-500' : 'bg-red-900 border-red-500'}`}>
<div className="flex justify-between items-center">
<span className="font-semibold">
{lastResult.is_correct ? '✅ Correct!' : '❌ Incorrect'}
</span>
{lastResult.difficulty_changed && (
<span className="text-sm bg-blue-900 px-2 py-1 rounded">
Level: {lastResult.previous_difficulty} {lastResult.new_difficulty}
</span>
)}
</div>
<p className="text-sm mt-1">{lastResult.explanation}</p>
{lastResult.llm_prediction && (
<div className="mt-2 text-sm bg-black bg-opacity-30 p-2 rounded">
🤖 AI predicted: {lastResult.llm_prediction.llm_prediction}
{lastResult.llm_agrees ? ' ✅ (Agreed)' : ' ❌ (Disagreed)'}
<span className="ml-2 text-gray-400">
({lastResult.llm_prediction.confidence * 100}% confidence)
</span>
</div>
)}
</div>
)}
{/* Current Question */}
{currentQuestion && (
<div className="bg-gray-800 p-6 rounded-lg mb-6">
<div className="flex justify-between items-start mb-4">
<div>
<div className="flex items-center space-x-2 mb-2">
<span className={`px-2 py-1 rounded text-xs font-medium ${getDifficultyColor(currentQuestion.difficulty)}`}>
{currentQuestion.difficulty.toUpperCase()}
</span>
<span className="text-sm text-gray-400">
{currentQuestion.category}
</span>
</div>
<h2 className="text-xl font-semibold">
{currentQuestion.question_text}
</h2>
</div>
<button
onClick={getAIPrediction}
className="bg-purple-600 hover:bg-purple-700 px-3 py-1 rounded text-sm flex items-center space-x-1"
>
<Brain className="h-4 w-4" />
<span>AI Hint</span>
</button>
</div>
{/* AI Prediction */}
{showPrediction && aiPrediction && (
<div className="bg-purple-900 bg-opacity-30 border border-purple-600 p-4 rounded mb-4">
<h3 className="font-semibold mb-2 flex items-center space-x-2">
<Brain className="h-4 w-4" />
<span>🤖 AI Prediction</span>
</h3>
<p className="text-sm">
AI suggests: <strong>{aiPrediction.llm_prediction}</strong>
</p>
<div className="flex justify-between text-xs text-gray-400 mt-1">
<span>Confidence: {(aiPrediction.confidence * 100).toFixed(1)}%</span>
<span>{aiPrediction.fallback_mode ? '(Fallback mode)' : '(CNN model)'}</span>
</div>
</div>
)}
{/* Answer Choices */}
<div className="space-y-3">
{Object.entries(currentQuestion.choices).map(([letter, text]) => (
<button
key={letter}
onClick={() => setSelectedAnswer(letter)}
className={`w-full p-4 text-left rounded-lg border transition-colors ${
selectedAnswer === letter
? 'bg-blue-900 border-blue-500 text-blue-100'
: 'bg-gray-700 border-gray-600 hover:bg-gray-600'
}`}
>
<div className="flex items-center space-x-3">
<span className="w-6 h-6 rounded-full border-2 border-gray-400 flex items-center justify-center text-sm font-bold">
{letter}
</span>
<span>{text}</span>
</div>
</button>
))}
</div>
<button
onClick={submitAnswer}
disabled={!selectedAnswer || loading}
className="mt-6 w-full bg-gradient-to-r from-green-600 to-blue-600 hover:from-green-700 hover:to-blue-700 disabled:from-gray-600 disabled:to-gray-600 p-4 rounded-lg font-semibold flex items-center justify-center space-x-2"
>
{loading ? (
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
) : (
<>
<span>Submit Answer</span>
<ChevronRight className="h-5 w-5" />
</>
)}
</button>
</div>
)}
</div>
</div>
)
}
+826
View File
@@ -0,0 +1,826 @@
'use client'
import React, { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { Users, Plus, Trash2, Play, Square, Settings, Brain, Crown, Target } from 'lucide-react'
interface Question {
question_id: string
question_text: string
options: string[]
correct_answer: string
difficulty: 'easy' | 'medium' | 'hard'
points: number
explanation: string
}
interface Participant {
session_id: string
username: string
score: number
current_difficulty: string
total_questions: number
correct_answers: number
status: string
}
interface QuizRoom {
room_id: string
room_code: string
title: string
host_name: string
is_private: boolean
status: string
questions: Question[]
participants: Participant[]
max_participants: number
duration_minutes: number
participants_count?: number
questions_count?: number
questions_by_difficulty?: {
easy: number
medium: number
hard: number
}
}
export default function QuizHostPanel() {
const router = useRouter()
const [currentRoom, setCurrentRoom] = useState<QuizRoom | null>(null)
const [activeTab, setActiveTab] = useState<'setup' | 'questions' | 'participants' | 'live'>('setup')
const [showCreateRoom, setShowCreateRoom] = useState(false)
const [showAddQuestion, setShowAddQuestion] = useState(false)
const [showAIGenerate, setShowAIGenerate] = useState(false)
// Room creation form
const [roomForm, setRoomForm] = useState({
host_name: '',
room_title: '',
is_private: false,
max_participants: 50,
duration_minutes: 30
})
// Question form
const [questionForm, setQuestionForm] = useState({
question_text: '',
options: ['', '', '', ''],
correct_answer: '',
difficulty: 'medium' as 'easy' | 'medium' | 'hard',
points: 10,
explanation: ''
})
// AI generation form
const [aiForm, setAiForm] = useState({
topic: '',
num_easy: 3,
num_medium: 3,
num_hard: 2
})
const createRoom = async () => {
try {
const response = await fetch('http://127.0.0.1:5000/api/quizzes/create-room', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(roomForm)
})
const data = await response.json()
console.log('Room creation response:', data) // Debug log
if (data.success) {
// Ensure the room has all required properties
const room = {
...data.room,
status: data.room.status || 'waiting',
participants: data.room.participants || [],
questions: data.room.questions || []
}
console.log('Room object:', room) // Debug log
setCurrentRoom(room)
setShowCreateRoom(false)
setActiveTab('questions')
alert(`🎉 Room created! Code: ${room.room_code}`)
} else {
alert(`Error: ${data.error}`)
}
} catch (error) {
console.error('Room creation error:', error)
alert('Network error: Could not create room')
}
}
const addQuestion = async () => {
if (!currentRoom) return
if (!questionForm.question_text || questionForm.options.some(opt => !opt.trim()) || !questionForm.correct_answer) {
alert('Please fill all question fields')
return
}
try {
const response = await fetch(`http://127.0.0.1:5000/api/quizzes/room/${currentRoom.room_code}/add-question`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(questionForm)
})
const data = await response.json()
if (data.success) {
// Refresh room data
fetchRoomData()
setShowAddQuestion(false)
setQuestionForm({
question_text: '',
options: ['', '', '', ''],
correct_answer: '',
difficulty: 'medium',
points: 10,
explanation: ''
})
alert('✅ Question added successfully!')
} else {
alert(`Error: ${data.error}`)
}
} catch (error) {
alert('Network error: Could not add question')
}
}
const generateAIQuestions = async () => {
if (!currentRoom) return
try {
const response = await fetch(`http://127.0.0.1:5000/api/quizzes/room/${currentRoom.room_code}/generate-ai-questions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(aiForm)
})
const data = await response.json()
if (data.success) {
fetchRoomData()
setShowAIGenerate(false)
alert(`🤖 Generated ${data.questions.length} AI questions!`)
} else {
alert(`Error: ${data.error}`)
}
} catch (error) {
alert('Network error: Could not generate questions')
}
}
const removeQuestion = async (questionId: string) => {
if (!currentRoom || !confirm('Remove this question?')) return
try {
const response = await fetch(`http://127.0.0.1:5000/api/quizzes/room/${currentRoom.room_code}/remove-question/${questionId}`, {
method: 'DELETE'
})
const data = await response.json()
if (data.success) {
fetchRoomData()
alert('✅ Question removed')
} else {
alert(`Error: ${data.error}`)
}
} catch (error) {
alert('Network error: Could not remove question')
}
}
const removeParticipant = async (username: string) => {
if (!currentRoom || !confirm(`Remove ${username} from the quiz?`)) return
try {
const response = await fetch(`http://127.0.0.1:5000/api/quizzes/room/${currentRoom.room_code}/remove-participant/${username}`, {
method: 'DELETE'
})
const data = await response.json()
if (data.success) {
fetchRoomData()
alert(`✅ Removed ${username}`)
} else {
alert(`Error: ${data.error}`)
}
} catch (error) {
alert('Network error: Could not remove participant')
}
}
const startQuiz = async () => {
if (!currentRoom) return
if (currentRoom.questions.length === 0) {
alert('Add questions before starting the quiz!')
return
}
if (!confirm('Start the quiz now? Participants will begin answering questions.')) return
try {
const response = await fetch(`http://127.0.0.1:5000/api/quizzes/room/${currentRoom.room_code}/start`, {
method: 'POST'
})
const data = await response.json()
if (data.success) {
fetchRoomData()
setActiveTab('live')
alert('🚀 Quiz started!')
} else {
alert(`Error: ${data.error}`)
}
} catch (error) {
alert('Network error: Could not start quiz')
}
}
const endQuiz = async () => {
if (!currentRoom || !confirm('End the quiz now?')) return
try {
const response = await fetch(`http://127.0.0.1:5000/api/quizzes/room/${currentRoom.room_code}/end`, {
method: 'POST'
})
const data = await response.json()
if (data.success) {
fetchRoomData()
alert('✅ Quiz ended!')
} else {
alert(`Error: ${data.error}`)
}
} catch (error) {
alert('Network error: Could not end quiz')
}
}
const fetchRoomData = async () => {
if (!currentRoom) return
try {
const response = await fetch(`http://127.0.0.1:5000/api/quizzes/room/${currentRoom.room_code}/info`)
const data = await response.json()
if (data.success) {
setCurrentRoom(data.room)
}
} catch (error) {
console.error('Failed to fetch room data:', error)
}
}
// Poll for live updates when quiz is active
useEffect(() => {
if (currentRoom?.status === 'active') {
const interval = setInterval(fetchRoomData, 3000)
return () => clearInterval(interval)
}
}, [currentRoom?.status])
const getDifficultyColor = (difficulty: string) => {
switch (difficulty) {
case 'easy': return 'text-green-400 bg-green-900'
case 'medium': return 'text-yellow-400 bg-yellow-900'
case 'hard': return 'text-red-400 bg-red-900'
default: return 'text-gray-400 bg-gray-700'
}
}
const getStatusColor = (status: string) => {
switch (status) {
case 'waiting': return 'text-yellow-400 bg-yellow-900'
case 'active': return 'text-green-400 bg-green-900'
case 'completed': return 'text-gray-400 bg-gray-700'
default: return 'text-gray-400 bg-gray-700'
}
}
// Safe status getter
const roomStatus = currentRoom?.status || 'waiting'
if (!currentRoom) {
return (
<div className="min-h-screen bg-gray-900 text-white">
<div className="max-w-4xl mx-auto p-6">
<div className="text-center mb-8">
<Crown className="h-16 w-16 text-yellow-400 mx-auto mb-4" />
<h1 className="text-4xl font-bold mb-4">👑 Quiz Host Panel</h1>
<p className="text-gray-400">
Create and manage adaptive quizzes with AI-powered questions
</p>
</div>
<div className="bg-gray-800 p-6 rounded-lg">
<h2 className="text-xl font-bold mb-4">Create New Quiz Room</h2>
<div className="space-y-4">
<input
type="text"
placeholder="Your name (Host)"
value={roomForm.host_name}
onChange={(e) => setRoomForm(prev => ({...prev, host_name: e.target.value}))}
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
/>
<input
type="text"
placeholder="Quiz room title"
value={roomForm.room_title}
onChange={(e) => setRoomForm(prev => ({...prev, room_title: e.target.value}))}
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
/>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={roomForm.is_private}
onChange={(e) => setRoomForm(prev => ({...prev, is_private: e.target.checked}))}
className="rounded"
/>
<span>Private Room (requires code)</span>
</label>
</div>
<input
type="number"
placeholder="Max participants"
value={roomForm.max_participants}
onChange={(e) => setRoomForm(prev => ({...prev, max_participants: parseInt(e.target.value) || 50}))}
className="p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
min="1"
max="100"
/>
<input
type="number"
placeholder="Duration (minutes)"
value={roomForm.duration_minutes}
onChange={(e) => setRoomForm(prev => ({...prev, duration_minutes: parseInt(e.target.value) || 30}))}
className="p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
min="5"
max="180"
/>
</div>
<button
onClick={createRoom}
disabled={!roomForm.host_name || !roomForm.room_title}
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 disabled:from-gray-600 disabled:to-gray-600 p-4 rounded-lg font-semibold"
>
🚀 Create Quiz Room
</button>
</div>
</div>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gray-900 text-white">
<div className="max-w-7xl mx-auto p-6">
{/* Header */}
<div className="bg-gray-800 p-4 rounded-lg mb-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold flex items-center space-x-2">
<Crown className="h-6 w-6 text-yellow-400" />
<span>{currentRoom.title}</span>
</h1>
<div className="flex items-center space-x-4 text-sm text-gray-400 mt-1">
<span>Code: <span className="font-bold text-blue-400">{currentRoom.room_code}</span></span>
<span className={`px-2 py-1 rounded text-xs ${getStatusColor(roomStatus)}`}>
{roomStatus.toUpperCase()}
</span>
<span>👥 {currentRoom.participants?.length || 0}/{currentRoom.max_participants}</span>
<span> {currentRoom.questions?.length || 0} questions</span>
</div>
</div>
<div className="flex items-center space-x-2">
{roomStatus === 'waiting' && (
<button
onClick={startQuiz}
disabled={(currentRoom.questions?.length || 0) === 0}
className="bg-green-600 hover:bg-green-700 disabled:bg-gray-600 px-4 py-2 rounded flex items-center space-x-2"
>
<Play className="h-4 w-4" />
<span>Start Quiz</span>
</button>
)}
{roomStatus === 'active' && (
<button
onClick={endQuiz}
className="bg-red-600 hover:bg-red-700 px-4 py-2 rounded flex items-center space-x-2"
>
<Square className="h-4 w-4" />
<span>End Quiz</span>
</button>
)}
</div>
</div>
</div>
{/* Tabs */}
<div className="flex space-x-1 mb-6">
{[
{ id: 'questions', label: `Questions (${currentRoom.questions?.length || 0})`, icon: Target },
{ id: 'participants', label: `Participants (${currentRoom.participants?.length || 0})`, icon: Users },
{ id: 'live', label: 'Live View', icon: Play }
].map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`px-4 py-2 rounded flex items-center space-x-2 ${
activeTab === tab.id
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
<tab.icon className="h-4 w-4" />
<span>{tab.label}</span>
</button>
))}
</div>
{/* Questions Tab */}
{activeTab === 'questions' && (
<div>
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-bold">📝 Question Management</h2>
<div className="flex space-x-2">
<button
onClick={() => setShowAIGenerate(true)}
className="bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded flex items-center space-x-2"
>
<Brain className="h-4 w-4" />
<span>🤖 AI Generate</span>
</button>
<button
onClick={() => setShowAddQuestion(true)}
className="bg-green-600 hover:bg-green-700 px-4 py-2 rounded flex items-center space-x-2"
>
<Plus className="h-4 w-4" />
<span>Add Question</span>
</button>
</div>
</div>
{/* Questions by Difficulty */}
{['easy', 'medium', 'hard'].map(difficulty => {
const difficultyQuestions = (currentRoom.questions || []).filter(q => q.difficulty === difficulty)
return (
<div key={difficulty} className="mb-6">
<h3 className={`text-lg font-semibold mb-3 px-3 py-1 rounded inline-block ${getDifficultyColor(difficulty)}`}>
{difficulty.toUpperCase()} ({difficultyQuestions.length} questions)
</h3>
<div className="space-y-3">
{difficultyQuestions.map((question, index) => (
<div key={question.question_id} className="bg-gray-800 p-4 rounded-lg">
<div className="flex justify-between items-start">
<div className="flex-1">
<h4 className="font-semibold mb-2">{question.question_text}</h4>
<div className="grid grid-cols-2 gap-2 text-sm text-gray-400 mb-2">
{question.options.map((option, optIndex) => (
<span key={optIndex} className={`${option === question.correct_answer ? 'text-green-400 font-semibold' : ''}`}>
{String.fromCharCode(65 + optIndex)}) {option}
</span>
))}
</div>
<div className="text-xs text-gray-500">
Points: {question.points} | Correct: {question.correct_answer}
</div>
</div>
<button
onClick={() => removeQuestion(question.question_id)}
disabled={roomStatus !== 'waiting'}
className="text-red-400 hover:text-red-300 disabled:text-gray-600 ml-4"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
))}
{difficultyQuestions.length === 0 && (
<div className="text-center py-4 text-gray-500 border-2 border-dashed border-gray-700 rounded-lg">
No {difficulty} questions yet
</div>
)}
</div>
</div>
)
})}
{/* Add Question Modal */}
{showAddQuestion && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-gray-800 p-6 rounded-lg max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<h3 className="text-xl font-bold mb-4"> Add New Question</h3>
<div className="space-y-4">
<textarea
placeholder="Question text"
value={questionForm.question_text}
onChange={(e) => setQuestionForm(prev => ({...prev, question_text: e.target.value}))}
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none resize-none"
rows={3}
/>
<div className="space-y-2">
<label className="text-sm font-medium">Options:</label>
{questionForm.options.map((option, index) => (
<input
key={index}
type="text"
placeholder={`Option ${String.fromCharCode(65 + index)}`}
value={option}
onChange={(e) => {
const newOptions = [...questionForm.options]
newOptions[index] = e.target.value
setQuestionForm(prev => ({...prev, options: newOptions}))
}}
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
/>
))}
</div>
<div className="grid grid-cols-3 gap-4">
<input
type="text"
placeholder="Correct answer"
value={questionForm.correct_answer}
onChange={(e) => setQuestionForm(prev => ({...prev, correct_answer: e.target.value}))}
className="p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
/>
<select
value={questionForm.difficulty}
onChange={(e) => setQuestionForm(prev => ({...prev, difficulty: e.target.value as any}))}
className="p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
>
<option value="easy">🟢 Easy</option>
<option value="medium">🟡 Medium</option>
<option value="hard">🔴 Hard</option>
</select>
<input
type="number"
placeholder="Points"
value={questionForm.points}
onChange={(e) => setQuestionForm(prev => ({...prev, points: parseInt(e.target.value) || 10}))}
className="p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
min="1"
/>
</div>
<textarea
placeholder="Explanation (optional)"
value={questionForm.explanation}
onChange={(e) => setQuestionForm(prev => ({...prev, explanation: e.target.value}))}
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none resize-none"
rows={2}
/>
</div>
<div className="flex space-x-4 mt-6">
<button
onClick={addQuestion}
className="bg-green-600 hover:bg-green-700 px-6 py-2 rounded font-semibold"
>
Add Question
</button>
<button
onClick={() => setShowAddQuestion(false)}
className="bg-gray-600 hover:bg-gray-700 px-6 py-2 rounded"
>
Cancel
</button>
</div>
</div>
</div>
)}
{/* AI Generate Modal */}
{showAIGenerate && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-gray-800 p-6 rounded-lg max-w-md w-full mx-4">
<h3 className="text-xl font-bold mb-4 flex items-center space-x-2">
<Brain className="h-5 w-5 text-purple-400" />
<span>🤖 AI Question Generator</span>
</h3>
<div className="space-y-4">
<input
type="text"
placeholder="Topic (e.g., Programming, Science)"
value={aiForm.topic}
onChange={(e) => setAiForm(prev => ({...prev, topic: e.target.value}))}
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-purple-500 focus:outline-none"
/>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium mb-1">🟢 Easy</label>
<input
type="number"
value={aiForm.num_easy}
onChange={(e) => setAiForm(prev => ({...prev, num_easy: parseInt(e.target.value) || 0}))}
className="w-full p-2 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-purple-500 focus:outline-none"
min="0"
max="10"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">🟡 Medium</label>
<input
type="number"
value={aiForm.num_medium}
onChange={(e) => setAiForm(prev => ({...prev, num_medium: parseInt(e.target.value) || 0}))}
className="w-full p-2 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-purple-500 focus:outline-none"
min="0"
max="10"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">🔴 Hard</label>
<input
type="number"
value={aiForm.num_hard}
onChange={(e) => setAiForm(prev => ({...prev, num_hard: parseInt(e.target.value) || 0}))}
className="w-full p-2 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-purple-500 focus:outline-none"
min="0"
max="10"
/>
</div>
</div>
</div>
<div className="flex space-x-4 mt-6">
<button
onClick={generateAIQuestions}
disabled={aiForm.num_easy + aiForm.num_medium + aiForm.num_hard === 0}
className="bg-purple-600 hover:bg-purple-700 disabled:bg-gray-600 px-6 py-2 rounded font-semibold"
>
🚀 Generate
</button>
<button
onClick={() => setShowAIGenerate(false)}
className="bg-gray-600 hover:bg-gray-700 px-6 py-2 rounded"
>
Cancel
</button>
</div>
</div>
</div>
)}
</div>
)}
{/* Participants Tab */}
{activeTab === 'participants' && (
<div>
<h2 className="text-xl font-bold mb-6">👥 Participant Management</h2>
{(currentRoom.participants?.length || 0) === 0 ? (
<div className="text-center py-12 text-gray-400">
<Users className="h-16 w-16 mx-auto mb-4 opacity-50" />
<p className="text-xl mb-2">No participants yet</p>
<p>Share room code: <span className="font-bold text-blue-400">{currentRoom.room_code}</span></p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{(currentRoom.participants || []).map((participant) => (
<div key={participant.session_id} className="bg-gray-800 p-4 rounded-lg">
<div className="flex justify-between items-start mb-3">
<div>
<h3 className="font-semibold">{participant.username}</h3>
<div className="text-sm text-gray-400">
Score: {participant.score} pts
</div>
</div>
<button
onClick={() => removeParticipant(participant.username)}
disabled={roomStatus === 'active'}
className="text-red-400 hover:text-red-300 disabled:text-gray-600"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span>Difficulty:</span>
<span className={`px-2 py-1 rounded text-xs ${getDifficultyColor(participant.current_difficulty)}`}>
{participant.current_difficulty}
</span>
</div>
<div className="flex justify-between">
<span>Progress:</span>
<span>{participant.correct_answers}/{participant.total_questions}</span>
</div>
<div className="flex justify-between">
<span>Accuracy:</span>
<span>
{participant.total_questions > 0
? Math.round((participant.correct_answers / participant.total_questions) * 100)
: 0}%
</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
)}
{/* Live View Tab */}
{activeTab === 'live' && (
<div>
<h2 className="text-xl font-bold mb-6">📺 Live Quiz Dashboard</h2>
{roomStatus !== 'active' ? (
<div className="text-center py-12 text-gray-400">
<Play className="h-16 w-16 mx-auto mb-4 opacity-50" />
<p className="text-xl mb-2">Quiz not active</p>
<p>Start the quiz to see live updates</p>
</div>
) : (
<div className="space-y-6">
{/* Real-time Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-gray-800 p-4 rounded-lg text-center">
<div className="text-2xl font-bold text-blue-400">{currentRoom.participants?.length || 0}</div>
<div className="text-sm text-gray-400">Active Participants</div>
</div>
<div className="bg-gray-800 p-4 rounded-lg text-center">
<div className="text-2xl font-bold text-green-400">
{Math.round((currentRoom.participants || []).reduce((sum, p) => sum + (p.total_questions > 0 ? (p.correct_answers / p.total_questions) * 100 : 0), 0) / Math.max((currentRoom.participants || []).length, 1))}%
</div>
<div className="text-sm text-gray-400">Avg Accuracy</div>
</div>
<div className="bg-gray-800 p-4 rounded-lg text-center">
<div className="text-2xl font-bold text-purple-400">
{Math.max(...(currentRoom.participants || []).map(p => p.score), 0)}
</div>
<div className="text-sm text-gray-400">Top Score</div>
</div>
<div className="bg-gray-800 p-4 rounded-lg text-center">
<div className="text-2xl font-bold text-yellow-400">
{(currentRoom.participants || []).filter(p => p.current_difficulty === 'hard').length}
</div>
<div className="text-sm text-gray-400">Hard Level</div>
</div>
</div>
{/* Leaderboard */}
<div className="bg-gray-800 p-6 rounded-lg">
<h3 className="text-lg font-bold mb-4">🏆 Live Leaderboard</h3>
<div className="space-y-2">
{(currentRoom.participants || [])
.sort((a, b) => b.score - a.score)
.map((participant, index) => (
<div key={participant.session_id} className="flex items-center justify-between p-3 bg-gray-700 rounded">
<div className="flex items-center space-x-3">
<span className="font-bold text-yellow-400">#{index + 1}</span>
<span className="font-semibold">{participant.username}</span>
<span className={`px-2 py-1 rounded text-xs ${getDifficultyColor(participant.current_difficulty)}`}>
{participant.current_difficulty}
</span>
</div>
<div className="text-right">
<div className="font-bold">{participant.score} pts</div>
<div className="text-sm text-gray-400">
{participant.correct_answers}/{participant.total_questions} correct
</div>
</div>
</div>
))}
</div>
</div>
</div>
)}
</div>
)}
</div>
</div>
)
}
+238
View File
@@ -0,0 +1,238 @@
'use client'
import React, { useState } from 'react'
import { useRouter } from 'next/navigation'
import { Users, Lock, Globe, Search, Play } from 'lucide-react'
interface PublicRoom {
room_id: string
room_code: string
title: string
host_name: string
participants_count: number
max_participants: number
questions_count: number
status: string
}
export default function QuizJoinPage() {
const [joinMode, setJoinMode] = useState<'code' | 'public'>('public')
const [roomCode, setRoomCode] = useState('')
const [username, setUsername] = useState('')
const [publicRooms, setPublicRooms] = useState<PublicRoom[]>([])
const [loading, setLoading] = useState(false)
const router = useRouter()
React.useEffect(() => {
if (joinMode === 'public') {
fetchPublicRooms()
}
}, [joinMode])
const fetchPublicRooms = async () => {
try {
const response = await fetch('http://127.0.0.1:5000/api/quizzes/public-rooms')
const data = await response.json()
if (data.success) {
setPublicRooms(data.public_rooms)
}
} catch (error) {
console.error('Failed to fetch public rooms:', error)
}
}
const joinRoom = async (code: string) => {
if (!username.trim()) {
alert('Please enter your username')
return
}
setLoading(true)
try {
const response = await fetch('http://127.0.0.1:5000/api/quizzes/join-room', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
room_code: code,
username: username.trim()
})
})
const data = await response.json()
if (data.success) {
// Store session info and redirect to quiz
localStorage.setItem('quiz_session', JSON.stringify(data.session))
router.push(`/quiz-play/${data.session.session_id}`)
} else {
alert(`Error: ${data.error}`)
}
} catch (error) {
alert('Network error: Could not join room')
} finally {
setLoading(false)
}
}
const joinWithCode = () => {
if (!roomCode.trim()) {
alert('Please enter room code')
return
}
joinRoom(roomCode.trim().toUpperCase())
}
return (
<div className="min-h-screen bg-gray-900 text-white">
<div className="max-w-4xl mx-auto p-6">
<div className="text-center mb-8">
<Users className="h-16 w-16 text-blue-400 mx-auto mb-4" />
<h1 className="text-4xl font-bold mb-4">🎯 Join Quiz</h1>
<p className="text-gray-400">
Join an adaptive quiz and test your knowledge!
</p>
</div>
{/* Username Input */}
<div className="bg-gray-800 p-6 rounded-lg mb-6">
<h2 className="text-xl font-bold mb-4">👤 Enter Your Name</h2>
<input
type="text"
placeholder="Your username"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
maxLength={20}
/>
</div>
{/* Join Mode Toggle */}
<div className="flex space-x-1 mb-6">
<button
onClick={() => setJoinMode('public')}
className={`flex-1 p-4 rounded-lg flex items-center justify-center space-x-2 ${
joinMode === 'public'
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
<Globe className="h-5 w-5" />
<span>Public Rooms</span>
</button>
<button
onClick={() => setJoinMode('code')}
className={`flex-1 p-4 rounded-lg flex items-center justify-center space-x-2 ${
joinMode === 'code'
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
<Lock className="h-5 w-5" />
<span>Private Code</span>
</button>
</div>
{/* Join with Code */}
{joinMode === 'code' && (
<div className="bg-gray-800 p-6 rounded-lg">
<h2 className="text-xl font-bold mb-4 flex items-center space-x-2">
<Lock className="h-5 w-5 text-yellow-400" />
<span>🔐 Join with Room Code</span>
</h2>
<div className="flex space-x-4">
<input
type="text"
placeholder="Enter room code (e.g., ABC123)"
value={roomCode}
onChange={(e) => setRoomCode(e.target.value.toUpperCase())}
className="flex-1 p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
maxLength={6}
/>
<button
onClick={joinWithCode}
disabled={!username.trim() || !roomCode.trim() || loading}
className="bg-green-600 hover:bg-green-700 disabled:bg-gray-600 px-6 py-3 rounded font-semibold flex items-center space-x-2"
>
{loading ? (
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
) : (
<>
<Play className="h-4 w-4" />
<span>Join</span>
</>
)}
</button>
</div>
</div>
)}
{/* Public Rooms */}
{joinMode === 'public' && (
<div className="bg-gray-800 p-6 rounded-lg">
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-bold flex items-center space-x-2">
<Globe className="h-5 w-5 text-green-400" />
<span>🌍 Public Quiz Rooms</span>
</h2>
<button
onClick={fetchPublicRooms}
className="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded flex items-center space-x-2"
>
<Search className="h-4 w-4" />
<span>Refresh</span>
</button>
</div>
{publicRooms.length === 0 ? (
<div className="text-center py-12 text-gray-400">
<Globe className="h-16 w-16 mx-auto mb-4 opacity-50" />
<p className="text-xl mb-2">No public rooms available</p>
<p>Create your own room or join with a private code</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{publicRooms.map((room) => (
<div key={room.room_id} className="bg-gray-700 p-4 rounded-lg hover:bg-gray-650 transition-colors">
<div className="flex justify-between items-start mb-3">
<div>
<h3 className="font-semibold text-lg">{room.title}</h3>
<p className="text-sm text-gray-400">Host: {room.host_name}</p>
</div>
<span className={`px-2 py-1 rounded text-xs ${
room.status === 'waiting' ? 'bg-yellow-900 text-yellow-400' : 'bg-green-900 text-green-400'
}`}>
{room.status.toUpperCase()}
</span>
</div>
<div className="flex justify-between items-center text-sm text-gray-400 mb-4">
<span>👥 {room.participants_count}/{room.max_participants}</span>
<span> {room.questions_count} questions</span>
<span>🔢 {room.room_code}</span>
</div>
<button
onClick={() => joinRoom(room.room_code)}
disabled={!username.trim() || loading || room.participants_count >= room.max_participants}
className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 p-3 rounded font-semibold flex items-center justify-center space-x-2"
>
{loading ? (
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
) : (
<>
<Play className="h-4 w-4" />
<span>Join Quiz</span>
</>
)}
</button>
</div>
))}
</div>
)}
</div>
)}
</div>
</div>
)
}
+389
View File
@@ -0,0 +1,389 @@
'use client'
import React, { useState, useEffect } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { Brain, Trophy, Target, ArrowRight, CheckCircle, XCircle } from 'lucide-react'
interface Question {
question_id: string
question_text: string
options: string[]
correct_answer: string
difficulty: string
points: number
explanation: string
}
interface SessionStats {
current_difficulty?: string
consecutive_correct?: {
easy: number
medium: number
hard: number
}
total_questions?: number
correct_answers?: number
score?: number
accuracy?: number
}
export default function QuizPlayPage() {
const params = useParams()
const router = useRouter()
const sessionId = params.sessionId as string
const [currentQuestion, setCurrentQuestion] = useState<Question | null>(null)
const [sessionStats, setSessionStats] = useState<SessionStats>({
current_difficulty: 'easy',
consecutive_correct: { easy: 0, medium: 0, hard: 0 },
total_questions: 0,
correct_answers: 0,
score: 0,
accuracy: 0
})
const [selectedAnswer, setSelectedAnswer] = useState<string>('')
const [showResult, setShowResult] = useState(false)
const [lastResult, setLastResult] = useState<any>(null)
const [loading, setLoading] = useState(true)
const [quizCompleted, setQuizCompleted] = useState(false)
// ✅ Safe getter for current difficulty with fallback
const getCurrentDifficulty = () => {
return sessionStats?.current_difficulty || 'easy'
}
// ✅ Safe getter for consecutive correct with fallback
const getConsecutiveCorrect = () => {
return sessionStats?.consecutive_correct || { easy: 0, medium: 0, hard: 0 }
}
// Fetch next question
const fetchNextQuestion = async () => {
try {
setLoading(true)
const response = await fetch(`http://127.0.0.1:5000/api/quizzes/session/${sessionId}/next-question`)
const data = await response.json()
console.log('Next question response:', data) // ✅ Debug log
if (data.success) {
if (data.quiz_completed) {
setQuizCompleted(true)
setCurrentQuestion(null)
} else {
setCurrentQuestion(data.question)
// ✅ Safely update session stats with fallbacks
setSessionStats(prev => ({
current_difficulty: data.session_stats?.current_difficulty || prev.current_difficulty || 'easy',
consecutive_correct: data.session_stats?.consecutive_correct || prev.consecutive_correct || { easy: 0, medium: 0, hard: 0 },
total_questions: data.session_stats?.total_questions || prev.total_questions || 0,
correct_answers: data.session_stats?.correct_answers || prev.correct_answers || 0,
score: data.session_stats?.score || prev.score || 0,
accuracy: data.session_stats?.accuracy || prev.accuracy || 0
}))
}
} else {
alert(`Error: ${data.error}`)
}
} catch (error) {
console.error('Fetch question error:', error)
alert('Failed to fetch next question')
} finally {
setLoading(false)
}
}
// Submit answer
const submitAnswer = async () => {
if (!selectedAnswer || !currentQuestion) return
try {
setLoading(true)
const response = await fetch(`http://127.0.0.1:5000/api/quizzes/session/${sessionId}/submit-answer`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
answer: selectedAnswer,
question_data: currentQuestion
})
})
const data = await response.json()
console.log('Submit answer response:', data) // ✅ Debug log
if (data.success) {
setLastResult(data)
setShowResult(true)
// ✅ Safely update session stats with fallbacks
setSessionStats(prev => ({
current_difficulty: data.session_stats?.current_difficulty || prev.current_difficulty || 'easy',
consecutive_correct: data.session_stats?.consecutive_correct || prev.consecutive_correct || { easy: 0, medium: 0, hard: 0 },
total_questions: data.session_stats?.total_questions || prev.total_questions || 0,
correct_answers: data.session_stats?.correct_answers || prev.correct_answers || 0,
score: data.session_stats?.score || prev.score || 0,
accuracy: data.session_stats?.accuracy || prev.accuracy || 0
}))
} else {
alert(`Error: ${data.error}`)
}
} catch (error) {
console.error('Submit answer error:', error)
alert('Failed to submit answer')
} finally {
setLoading(false)
}
}
// Continue to next question
const continueToNext = () => {
setShowResult(false)
setSelectedAnswer('')
setLastResult(null)
fetchNextQuestion()
}
// Initial load
useEffect(() => {
if (sessionId) {
fetchNextQuestion()
}
}, [sessionId])
const getDifficultyColor = (difficulty: string) => {
switch (difficulty) {
case 'easy': return 'text-green-400 bg-green-900'
case 'medium': return 'text-yellow-400 bg-yellow-900'
case 'hard': return 'text-red-400 bg-red-900'
default: return 'text-gray-400 bg-gray-700'
}
}
if (loading && !currentQuestion) {
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-purple-600 mx-auto mb-4"></div>
<p>Loading question...</p>
</div>
</div>
)
}
if (quizCompleted) {
return (
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
<div className="max-w-2xl mx-auto p-6 text-center">
<Trophy className="h-16 w-16 text-yellow-400 mx-auto mb-4" />
<h1 className="text-3xl font-bold mb-4">🎉 Quiz Completed!</h1>
<div className="bg-gray-800 p-6 rounded-lg mb-6">
<h2 className="text-xl font-bold mb-4">Final Results</h2>
<div className="grid grid-cols-2 gap-4 text-center">
<div className="bg-gray-700 p-4 rounded">
<div className="text-2xl font-bold text-blue-400">{sessionStats.score || 0}</div>
<div className="text-gray-400">Final Score</div>
</div>
<div className="bg-gray-700 p-4 rounded">
<div className="text-2xl font-bold text-green-400">{sessionStats.accuracy || 0}%</div>
<div className="text-gray-400">Accuracy</div>
</div>
<div className="bg-gray-700 p-4 rounded">
<div className="text-2xl font-bold text-purple-400">{sessionStats.total_questions || 0}</div>
<div className="text-gray-400">Questions</div>
</div>
<div className="bg-gray-700 p-4 rounded">
<div className={`text-2xl font-bold px-3 py-1 rounded ${getDifficultyColor(getCurrentDifficulty())}`}>
{getCurrentDifficulty().toUpperCase()}
</div>
<div className="text-gray-400">Final Level</div>
</div>
</div>
</div>
<button
onClick={() => router.push('/quizzes')}
className="bg-blue-600 hover:bg-blue-700 px-6 py-3 rounded-lg font-semibold"
>
Back to Quizzes
</button>
</div>
</div>
)
}
if (showResult && lastResult) {
return (
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
<div className="max-w-2xl mx-auto p-6">
<div className="text-center mb-6">
{lastResult.is_correct ? (
<CheckCircle className="h-16 w-16 text-green-400 mx-auto mb-4" />
) : (
<XCircle className="h-16 w-16 text-red-400 mx-auto mb-4" />
)}
<h1 className="text-3xl font-bold mb-2">
{lastResult.is_correct ? '✅ Correct!' : '❌ Incorrect'}
</h1>
<p className="text-gray-400">
{lastResult.is_correct ? 'Great job!' : 'Keep trying!'}
</p>
</div>
<div className="bg-gray-800 p-6 rounded-lg mb-6">
<h3 className="font-semibold mb-2">Correct Answer:</h3>
<p className="text-green-400 mb-4">{lastResult.correct_answer}</p>
{lastResult.explanation && (
<div>
<h3 className="font-semibold mb-2">Explanation:</h3>
<p className="text-gray-300">{lastResult.explanation}</p>
</div>
)}
</div>
{/* Difficulty Change Notification */}
{lastResult.difficulty_changed && (
<div className="bg-blue-900 border border-blue-600 p-4 rounded-lg mb-6">
<h3 className="font-semibold mb-2">📈 Difficulty Updated!</h3>
<p>
Moved from <span className={`px-2 py-1 rounded ${getDifficultyColor(lastResult.previous_difficulty)}`}>
{lastResult.previous_difficulty}
</span> to <span className={`px-2 py-1 rounded ${getDifficultyColor(lastResult.new_difficulty)}`}>
{lastResult.new_difficulty}
</span>
</p>
</div>
)}
{/* Session Stats */}
<div className="bg-gray-800 p-6 rounded-lg mb-6">
<h3 className="font-semibold mb-4">📊 Your Progress</h3>
<div className="grid grid-cols-2 gap-4 text-center">
<div>
<div className="text-xl font-bold text-blue-400">{sessionStats.score || 0}</div>
<div className="text-gray-400 text-sm">Score</div>
</div>
<div>
<div className="text-xl font-bold text-green-400">{sessionStats.accuracy || 0}%</div>
<div className="text-gray-400 text-sm">Accuracy</div>
</div>
</div>
</div>
{/* AI Feedback */}
{lastResult.ai_feedback && (
<div className="bg-purple-900 border border-purple-600 p-4 rounded-lg mb-6">
<h3 className="font-semibold mb-2 flex items-center space-x-2">
<Brain className="h-5 w-5" />
<span>🤖 AI Analysis</span>
</h3>
<p className="text-purple-200">
AI predicted: <span className="font-semibold">{lastResult.ai_feedback.ai_prediction}</span>
{lastResult.ai_feedback.ai_agrees ? ' ✅ (Agrees with correct answer)' : ' ❌ (Disagrees)'}
</p>
<p className="text-xs text-purple-300 mt-1">
Confidence: {Math.round(lastResult.ai_feedback.ai_confidence * 100)}%
</p>
</div>
)}
<button
onClick={continueToNext}
className="w-full bg-blue-600 hover:bg-blue-700 p-4 rounded-lg font-semibold flex items-center justify-center space-x-2"
>
<span>Continue to Next Question</span>
<ArrowRight className="h-5 w-5" />
</button>
</div>
</div>
)
}
if (!currentQuestion) {
return (
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
<div className="text-center">
<p>No question available</p>
<button
onClick={() => router.push('/quizzes')}
className="mt-4 bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded"
>
Back to Quizzes
</button>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gray-900 text-white">
<div className="max-w-4xl mx-auto p-6">
{/* Header with Stats */}
<div className="bg-gray-800 p-4 rounded-lg mb-6">
<div className="flex justify-between items-center">
<div className="flex items-center space-x-4">
<span className={`px-3 py-1 rounded font-semibold ${getDifficultyColor(getCurrentDifficulty())}`}>
{getCurrentDifficulty().toUpperCase()}
</span>
<span className="text-gray-400">
Question {(sessionStats.total_questions || 0) + 1}
</span>
</div>
<div className="flex items-center space-x-6 text-sm">
<div className="text-center">
<div className="font-bold text-blue-400">{sessionStats.score || 0}</div>
<div className="text-gray-400">Score</div>
</div>
<div className="text-center">
<div className="font-bold text-green-400">{sessionStats.accuracy || 0}%</div>
<div className="text-gray-400">Accuracy</div>
</div>
<div className="text-center">
<div className="font-bold text-purple-400">{getConsecutiveCorrect()[getCurrentDifficulty()] || 0}</div>
<div className="text-gray-400">Streak</div>
</div>
</div>
</div>
</div>
{/* Question */}
<div className="bg-gray-800 p-6 rounded-lg mb-6">
<h1 className="text-2xl font-bold mb-6">{currentQuestion.question_text}</h1>
<div className="space-y-3">
{currentQuestion.options.map((option, index) => (
<button
key={index}
onClick={() => setSelectedAnswer(option)}
className={`w-full p-4 text-left rounded-lg border-2 transition-colors ${
selectedAnswer === option
? 'border-blue-500 bg-blue-900'
: 'border-gray-600 bg-gray-700 hover:border-gray-500'
}`}
>
<span className="font-semibold mr-3">{String.fromCharCode(65 + index)})</span>
{option}
</button>
))}
</div>
</div>
{/* Submit Button */}
<button
onClick={submitAnswer}
disabled={!selectedAnswer || loading}
className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 p-4 rounded-lg font-semibold flex items-center justify-center space-x-2"
>
{loading ? (
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
) : (
<>
<span>Submit Answer</span>
<ArrowRight className="h-5 w-5" />
</>
)}
</button>
</div>
</div>
)
}
+327 -6
View File
@@ -1,11 +1,332 @@
import { QuizRunner } from "@/components/quiz-runner" 'use client'
import React, { useState, useEffect } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { Brain, Clock, CheckCircle, XCircle, Sparkles, AlertCircle } from 'lucide-react'
interface QuizPageProps { interface Question {
params: { id: string
quizId: string question_number: number
question_text: string
options: string[]
correct_answer: string
points: number
ai_prediction?: any
}
interface Quiz {
id: string
title: string
description: string
questions: Question[]
generated_by?: string
total_points: number
}
export default function QuizTaking() {
const params = useParams()
const router = useRouter()
const quizId = params.quizId as string
const [quiz, setQuiz] = useState<Quiz | null>(null)
const [currentQuestion, setCurrentQuestion] = useState(0)
const [answers, setAnswers] = useState<Record<string, string>>({})
const [loading, setLoading] = useState(true)
const [submitting, setSubmitting] = useState(false)
const [results, setResults] = useState<any>(null)
const [showAIHint, setShowAIHint] = useState(false)
const [aiPrediction, setAIPrediction] = useState<any>(null)
const [error, setError] = useState('')
useEffect(() => {
fetchQuiz()
}, [quizId])
const fetchQuiz = async () => {
try {
const response = await fetch(`http://127.0.0.1:5000/api/quizzes/${quizId}`)
const data = await response.json()
if (data.success) {
setQuiz(data.quiz)
} else {
setError(data.error || 'Quiz not found')
}
} catch (err) {
setError('Failed to load quiz')
} finally {
setLoading(false)
} }
} }
export default function QuizPage({ params }: QuizPageProps) { const getAIHint = async () => {
return <QuizRunner quizId={params.quizId} /> if (!quiz || !quiz.questions[currentQuestion]) return
try {
setShowAIHint(true)
const response = await fetch('http://127.0.0.1:5000/api/quizzes/ai-predict', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
question_text: quiz.questions[currentQuestion].question_text
})
})
const data = await response.json()
if (data.success) {
setAIPrediction(data.prediction)
}
} catch (err) {
console.error('Failed to get AI hint:', err)
}
}
const handleAnswerSelect = (questionId: string, answer: string) => {
setAnswers(prev => ({ ...prev, [questionId]: answer }))
}
const submitQuiz = async () => {
if (!quiz) return
const unanswered = quiz.questions.filter(q => !answers[q.id])
if (unanswered.length > 0) {
if (!confirm(`You have ${unanswered.length} unanswered questions. Submit anyway?`)) {
return
}
}
setSubmitting(true)
try {
const response = await fetch(`http://127.0.0.1:5000/api/quizzes/${quizId}/submit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
answers,
participant_name: 'User' // You can get this from auth context
})
})
const data = await response.json()
if (data.success) {
setResults(data.results)
} else {
setError(data.error || 'Failed to submit quiz')
}
} catch (err) {
setError('Failed to submit quiz')
} finally {
setSubmitting(false)
}
}
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-purple-600 mx-auto mb-4"></div>
<p>Loading AI Quiz...</p>
</div>
</div>
)
}
if (error) {
return (
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
<div className="text-center">
<AlertCircle className="h-12 w-12 text-red-400 mx-auto mb-4" />
<p className="text-xl mb-4">{error}</p>
<button
onClick={() => router.push('/quizzes')}
className="bg-blue-600 hover:bg-blue-700 px-6 py-2 rounded"
>
Back to Quizzes
</button>
</div>
</div>
)
}
if (results) {
return (
<div className="min-h-screen bg-gray-900 text-white">
<div className="max-w-4xl mx-auto p-6">
<div className="text-center mb-8">
<div className="text-6xl mb-4">
{results.score >= 80 ? '🏆' : results.score >= 60 ? '🎉' : '📚'}
</div>
<h1 className="text-3xl font-bold mb-2">Quiz Complete!</h1>
<p className="text-xl text-gray-300">
You scored {results.score}% ({results.correct_answers}/{results.total_questions})
</p>
</div>
{/* AI Feedback */}
{results.ai_feedback && (
<div className="bg-gray-800 rounded-lg p-6 mb-6">
<h2 className="text-xl font-bold mb-4 flex items-center space-x-2">
<Brain className="h-5 w-5 text-purple-400" />
<span>🤖 AI Feedback</span>
</h2>
<div className="space-y-4">
{results.ai_feedback.map((feedback: any, index: number) => (
<div key={index} className="bg-gray-900 p-4 rounded border-l-4 border-purple-500">
<h3 className="font-semibold mb-2">Question {index + 1}</h3>
<p className="text-sm text-gray-300 mb-2">{feedback.question}</p>
<div className="flex items-center space-x-2 mb-2">
{feedback.is_correct ? (
<CheckCircle className="h-4 w-4 text-green-400" />
) : (
<XCircle className="h-4 w-4 text-red-400" />
)}
<span className="text-sm">
Your answer: {feedback.user_answer}
</span>
</div>
{feedback.ai_feedback && (
<p className="text-sm text-purple-300 bg-purple-900 bg-opacity-30 p-2 rounded">
🤖 {feedback.ai_feedback.feedback}
</p>
)}
</div>
))}
</div>
</div>
)}
<div className="text-center">
<button
onClick={() => router.push('/quizzes')}
className="bg-blue-600 hover:bg-blue-700 px-8 py-3 rounded-lg font-semibold"
>
Back to Quizzes
</button>
</div>
</div>
</div>
)
}
if (!quiz) return null
const question = quiz.questions[currentQuestion]
const progress = ((currentQuestion + 1) / quiz.questions.length) * 100
return (
<div className="min-h-screen bg-gray-900 text-white">
<div className="max-w-4xl mx-auto p-6">
{/* Header */}
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<h1 className="text-2xl font-bold flex items-center space-x-2">
{quiz.generated_by === 'AI' && <Brain className="h-6 w-6 text-purple-400" />}
<span>{quiz.title}</span>
</h1>
<div className="text-sm text-gray-400">
Question {currentQuestion + 1} of {quiz.questions.length}
</div>
</div>
{/* Progress Bar */}
<div className="w-full bg-gray-700 rounded-full h-2">
<div
className="bg-purple-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${progress}%` }}
></div>
</div>
</div>
{/* Question */}
<div className="bg-gray-800 rounded-lg p-6 mb-6">
<div className="flex justify-between items-start mb-4">
<h2 className="text-xl font-semibold">
{question.question_text}
</h2>
{quiz.generated_by === 'AI' && (
<button
onClick={getAIHint}
className="bg-purple-600 hover:bg-purple-700 px-3 py-1 rounded text-sm flex items-center space-x-1"
>
<Sparkles className="h-4 w-4" />
<span>AI Hint</span>
</button>
)}
</div>
{/* AI Hint */}
{showAIHint && aiPrediction && (
<div className="bg-purple-900 bg-opacity-30 border border-purple-600 p-4 rounded mb-4">
<h3 className="font-semibold mb-2 flex items-center space-x-2">
<Brain className="h-4 w-4" />
<span>🤖 AI Suggestion</span>
</h3>
<p className="text-sm">
AI predicts: <strong>{aiPrediction.predicted_answer}</strong>
</p>
<p className="text-xs text-gray-400 mt-1">
Confidence: {(aiPrediction.confidence * 100).toFixed(1)}%
</p>
</div>
)}
{/* Options */}
<div className="space-y-3">
{question.options.map((option, index) => (
<button
key={index}
onClick={() => handleAnswerSelect(question.id, option)}
className={`w-full p-4 text-left rounded-lg border transition-colors ${
answers[question.id] === option
? 'bg-purple-900 border-purple-500 text-purple-100'
: 'bg-gray-700 border-gray-600 hover:bg-gray-600'
}`}
>
<div className="flex items-center space-x-3">
<span className="w-6 h-6 rounded-full border-2 border-gray-400 flex items-center justify-center text-sm">
{String.fromCharCode(65 + index)}
</span>
<span>{option}</span>
</div>
</button>
))}
</div>
</div>
{/* Navigation */}
<div className="flex justify-between items-center">
<button
onClick={() => setCurrentQuestion(prev => Math.max(0, prev - 1))}
disabled={currentQuestion === 0}
className="bg-gray-700 hover:bg-gray-600 disabled:bg-gray-800 px-6 py-2 rounded"
>
Previous
</button>
<div className="text-sm text-gray-400">
{Object.keys(answers).length} of {quiz.questions.length} answered
</div>
{currentQuestion === quiz.questions.length - 1 ? (
<button
onClick={submitQuiz}
disabled={submitting}
className="bg-green-600 hover:bg-green-700 disabled:bg-gray-600 px-6 py-2 rounded flex items-center space-x-2"
>
{submitting ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
) : null}
<span>{submitting ? 'Submitting...' : 'Submit Quiz'}</span>
</button>
) : (
<button
onClick={() => setCurrentQuestion(prev => Math.min(quiz.questions.length - 1, prev + 1))}
className="bg-blue-600 hover:bg-blue-700 px-6 py-2 rounded"
>
Next
</button>
)}
</div>
</div>
</div>
)
} }
+251
View File
@@ -0,0 +1,251 @@
'use client'
import React, { useState } from 'react'
import { useRouter } from 'next/navigation'
import { Plus, Trash2, Save, ArrowLeft } from 'lucide-react'
interface Question {
question_text: string
options: string[]
correct_answer: string
points: number
}
export default function CreateQuizPage() {
const router = useRouter()
const [quiz, setQuiz] = useState({
title: '',
description: '',
difficulty: 'medium'
})
const [questions, setQuestions] = useState<Question[]>([])
const [currentQuestion, setCurrentQuestion] = useState<Question>({
question_text: '',
options: ['', '', '', ''],
correct_answer: '',
points: 10
})
const [loading, setLoading] = useState(false)
const addQuestion = () => {
if (!currentQuestion.question_text || currentQuestion.options.some(opt => !opt.trim()) || !currentQuestion.correct_answer) {
alert('Please fill all question fields')
return
}
setQuestions([...questions, { ...currentQuestion }])
setCurrentQuestion({
question_text: '',
options: ['', '', '', ''],
correct_answer: '',
points: 10
})
}
const removeQuestion = (index: number) => {
setQuestions(questions.filter((_, i) => i !== index))
}
const createQuiz = async () => {
if (!quiz.title || questions.length === 0) {
alert('Please add a title and at least one question')
return
}
setLoading(true)
try {
const quizData = {
...quiz,
questions: questions.map((q, index) => ({
...q,
id: `q_${index}`,
question_number: index + 1
})),
total_points: questions.reduce((sum, q) => sum + q.points, 0),
created_at: new Date().toISOString(),
generated_by: 'manual'
}
const response = await fetch('http://127.0.0.1:5000/api/quizzes/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(quizData)
})
const data = await response.json()
if (data.success) {
alert('✅ Quiz created successfully!')
router.push('/quizzes')
} else {
alert(`Error: ${data.error}`)
}
} catch (error) {
alert('Network error: Could not create quiz')
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen bg-gray-900 text-white">
<div className="max-w-4xl mx-auto p-6">
{/* Header */}
<div className="flex items-center space-x-4 mb-8">
<button
onClick={() => router.push('/quizzes')}
className="bg-gray-700 hover:bg-gray-600 p-2 rounded"
>
<ArrowLeft className="h-5 w-5" />
</button>
<h1 className="text-3xl font-bold">📝 Create New Quiz</h1>
</div>
{/* Quiz Details */}
<div className="bg-gray-800 p-6 rounded-lg mb-6">
<h2 className="text-xl font-bold mb-4">Quiz Information</h2>
<div className="space-y-4">
<input
type="text"
placeholder="Quiz title"
value={quiz.title}
onChange={(e) => setQuiz(prev => ({...prev, title: e.target.value}))}
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
/>
<textarea
placeholder="Quiz description"
value={quiz.description}
onChange={(e) => setQuiz(prev => ({...prev, description: e.target.value}))}
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none resize-none"
rows={3}
/>
<select
value={quiz.difficulty}
onChange={(e) => setQuiz(prev => ({...prev, difficulty: e.target.value}))}
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
>
<option value="easy">🟢 Easy</option>
<option value="medium">🟡 Medium</option>
<option value="hard">🔴 Hard</option>
</select>
</div>
</div>
{/* Add Question */}
<div className="bg-gray-800 p-6 rounded-lg mb-6">
<h2 className="text-xl font-bold mb-4">Add Question</h2>
<div className="space-y-4">
<textarea
placeholder="Question text"
value={currentQuestion.question_text}
onChange={(e) => setCurrentQuestion(prev => ({...prev, question_text: e.target.value}))}
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none resize-none"
rows={3}
/>
<div className="space-y-2">
<label className="text-sm font-medium">Options:</label>
{currentQuestion.options.map((option, index) => (
<input
key={index}
type="text"
placeholder={`Option ${String.fromCharCode(65 + index)}`}
value={option}
onChange={(e) => {
const newOptions = [...currentQuestion.options]
newOptions[index] = e.target.value
setCurrentQuestion(prev => ({...prev, options: newOptions}))
}}
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
/>
))}
</div>
<div className="grid grid-cols-2 gap-4">
<input
type="text"
placeholder="Correct answer"
value={currentQuestion.correct_answer}
onChange={(e) => setCurrentQuestion(prev => ({...prev, correct_answer: e.target.value}))}
className="p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
/>
<input
type="number"
placeholder="Points"
value={currentQuestion.points}
onChange={(e) => setCurrentQuestion(prev => ({...prev, points: parseInt(e.target.value) || 10}))}
className="p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
min="1"
/>
</div>
<button
onClick={addQuestion}
className="bg-green-600 hover:bg-green-700 px-6 py-2 rounded font-semibold flex items-center space-x-2"
>
<Plus className="h-4 w-4" />
<span>Add Question</span>
</button>
</div>
</div>
{/* Questions List */}
{questions.length > 0 && (
<div className="bg-gray-800 p-6 rounded-lg mb-6">
<h2 className="text-xl font-bold mb-4">Questions ({questions.length})</h2>
<div className="space-y-4">
{questions.map((question, index) => (
<div key={index} className="bg-gray-700 p-4 rounded-lg">
<div className="flex justify-between items-start">
<div className="flex-1">
<h3 className="font-semibold mb-2">Q{index + 1}: {question.question_text}</h3>
<div className="grid grid-cols-2 gap-2 text-sm text-gray-400 mb-2">
{question.options.map((option, optIndex) => (
<span key={optIndex} className={`${option === question.correct_answer ? 'text-green-400 font-semibold' : ''}`}>
{String.fromCharCode(65 + optIndex)}) {option}
</span>
))}
</div>
<div className="text-xs text-gray-500">
Points: {question.points} | Correct: {question.correct_answer}
</div>
</div>
<button
onClick={() => removeQuestion(index)}
className="text-red-400 hover:text-red-300 ml-4"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
))}
</div>
</div>
)}
{/* Create Button */}
<div className="text-center">
<button
onClick={createQuiz}
disabled={loading || !quiz.title || questions.length === 0}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 px-8 py-3 rounded-lg font-semibold flex items-center space-x-2 mx-auto"
>
{loading ? (
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
) : (
<>
<Save className="h-5 w-5" />
<span>Create Quiz</span>
</>
)}
</button>
</div>
</div>
</div>
)
}
+180
View File
@@ -0,0 +1,180 @@
'use client'
import React, { useState } from 'react'
import { useRouter } from 'next/navigation'
import { Brain, Sparkles, Settings, Clock, Trophy, AlertCircle } from 'lucide-react'
export default function AIQuizGenerator() {
const [loading, setLoading] = useState(false)
const [formData, setFormData] = useState({
topic: '',
difficulty: 'medium',
num_questions: 5
})
const [generatedQuiz, setGeneratedQuiz] = useState(null)
const [error, setError] = useState('')
const router = useRouter()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError('')
try {
const response = await fetch('http://127.0.0.1:5000/api/quizzes/generate-ai', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
})
const data = await response.json()
if (data.success) {
setGeneratedQuiz(data.quiz)
// Redirect to the generated quiz
router.push(`/quizzes/${data.quiz.id}`)
} else {
setError(data.error || 'Failed to generate quiz')
}
} catch (err) {
setError('Network error: Could not generate quiz')
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen bg-gray-900 text-white">
<div className="max-w-4xl mx-auto p-6">
{/* Header */}
<div className="text-center mb-8">
<div className="flex items-center justify-center space-x-3 mb-4">
<Brain className="h-12 w-12 text-purple-400" />
<Sparkles className="h-8 w-8 text-yellow-400" />
</div>
<h1 className="text-3xl font-bold mb-2">🤖 AI Quiz Generator</h1>
<p className="text-gray-400">
Generate intelligent quizzes using our trained CNN model
</p>
</div>
{/* Error Display */}
{error && (
<div className="bg-red-900 border border-red-600 p-4 rounded-lg mb-6 flex items-center space-x-2">
<AlertCircle className="h-5 w-5 text-red-400" />
<span>{error}</span>
</div>
)}
{/* Generator Form */}
<div className="bg-gray-800 rounded-lg p-6 mb-6">
<h2 className="text-xl font-bold mb-4 flex items-center space-x-2">
<Settings className="h-5 w-5 text-blue-400" />
<span>Quiz Configuration</span>
</h2>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Topic Input */}
<div>
<label className="block text-sm font-medium mb-2">
Topic/Subject
</label>
<input
type="text"
value={formData.topic}
onChange={(e) => setFormData(prev => ({...prev, topic: e.target.value}))}
placeholder="e.g., Science, History, Technology"
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-purple-500 focus:outline-none"
required
/>
<p className="text-sm text-gray-400 mt-1">
AI will generate questions related to this topic
</p>
</div>
{/* Difficulty Selection */}
<div>
<label className="block text-sm font-medium mb-2">
Difficulty Level
</label>
<select
value={formData.difficulty}
onChange={(e) => setFormData(prev => ({...prev, difficulty: e.target.value}))}
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-purple-500 focus:outline-none"
>
<option value="easy">🟢 Easy</option>
<option value="medium">🟡 Medium</option>
<option value="hard">🔴 Hard</option>
</select>
</div>
{/* Number of Questions */}
<div>
<label className="block text-sm font-medium mb-2">
Number of Questions
</label>
<div className="flex items-center space-x-4">
<input
type="range"
min="3"
max="20"
value={formData.num_questions}
onChange={(e) => setFormData(prev => ({...prev, num_questions: parseInt(e.target.value)}))}
className="flex-1"
/>
<span className="bg-gray-700 px-3 py-1 rounded font-bold">
{formData.num_questions}
</span>
</div>
</div>
{/* Generate Button */}
<button
type="submit"
disabled={loading}
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 disabled:from-gray-600 disabled:to-gray-600 p-4 rounded-lg font-semibold flex items-center justify-center space-x-2 transition-colors"
>
{loading ? (
<>
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
<span>Generating Quiz...</span>
</>
) : (
<>
<Brain className="h-5 w-5" />
<span>🚀 Generate AI Quiz</span>
</>
)}
</button>
</form>
</div>
{/* Features */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-gray-800 p-4 rounded-lg text-center">
<Brain className="h-8 w-8 text-purple-400 mx-auto mb-2" />
<h3 className="font-semibold mb-1">AI-Powered</h3>
<p className="text-sm text-gray-400">
Uses trained CNN model for intelligent question selection
</p>
</div>
<div className="bg-gray-800 p-4 rounded-lg text-center">
<Clock className="h-8 w-8 text-blue-400 mx-auto mb-2" />
<h3 className="font-semibold mb-1">Instant Generation</h3>
<p className="text-sm text-gray-400">
Generate quizzes in seconds with AI processing
</p>
</div>
<div className="bg-gray-800 p-4 rounded-lg text-center">
<Trophy className="h-8 w-8 text-yellow-400 mx-auto mb-2" />
<h3 className="font-semibold mb-1">Smart Feedback</h3>
<p className="text-sm text-gray-400">
AI provides intelligent feedback on answers
</p>
</div>
</div>
</div>
</div>
)
}
+433 -2
View File
@@ -1,5 +1,436 @@
import { QuizList } from "@/components/quiz-list" 'use client'
import React, { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { Brain, Plus, Clock, Trophy, Users, Sparkles, Crown, Target, Play, Globe, Lock } from 'lucide-react'
interface Quiz {
_id: string
id: string
title: string
description: string
difficulty: string
questions: any[]
generated_by?: string
created_at: string
total_points: number
}
interface QuizRoom {
room_id: string
room_code: string
title: string
host_name: string
is_private: boolean
status: string
participants_count: number
questions_count: number
questions_by_difficulty: {
easy: number
medium: number
hard: number
}
}
export default function QuizzesPage() { export default function QuizzesPage() {
return <QuizList /> const [activeTab, setActiveTab] = useState<'traditional' | 'rooms' | 'adaptive'>('rooms')
const [quizzes, setQuizzes] = useState<Quiz[]>([])
const [publicRooms, setPublicRooms] = useState<QuizRoom[]>([])
const [loading, setLoading] = useState(true)
const [aiAvailable, setAiAvailable] = useState(false)
const router = useRouter()
useEffect(() => {
if (activeTab === 'traditional') {
fetchTraditionalQuizzes()
} else if (activeTab === 'rooms') {
fetchPublicRooms()
}
}, [activeTab])
const fetchTraditionalQuizzes = async () => {
setLoading(true)
try {
const response = await fetch('http://127.0.0.1:5000/api/quizzes')
const data = await response.json()
if (data.success) {
setQuizzes(data.quizzes)
setAiAvailable(data.ai_available)
}
} catch (err) {
console.error('Failed to fetch quizzes:', err)
} finally {
setLoading(false)
}
}
const fetchPublicRooms = async () => {
setLoading(true)
try {
const response = await fetch('http://127.0.0.1:5000/api/quizzes/public-rooms')
const data = await response.json()
if (data.success) {
setPublicRooms(data.public_rooms)
}
} catch (err) {
console.error('Failed to fetch public rooms:', err)
} finally {
setLoading(false)
}
}
const getDifficultyColor = (difficulty: string) => {
switch (difficulty.toLowerCase()) {
case 'easy': return 'text-green-400 bg-green-900'
case 'medium': return 'text-yellow-400 bg-yellow-900'
case 'hard': return 'text-red-400 bg-red-900'
default: return 'text-gray-400 bg-gray-700'
}
}
const getStatusColor = (status: string) => {
switch (status) {
case 'waiting': return 'text-yellow-400 bg-yellow-900'
case 'active': return 'text-green-400 bg-green-900'
case 'completed': return 'text-gray-400 bg-gray-700'
default: return 'text-gray-400 bg-gray-700'
}
}
if (loading && activeTab === 'traditional' && quizzes.length === 0) {
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-purple-600 mx-auto mb-4"></div>
<p>Loading quizzes...</p>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gray-900 text-white">
<div className="max-w-7xl mx-auto p-6">
{/* Header */}
<div className="text-center mb-8">
<h1 className="text-4xl font-bold mb-4 flex items-center justify-center space-x-3">
<Trophy className="h-10 w-10 text-yellow-400" />
<span>🧠 OpenLearnX Quiz Platform</span>
</h1>
<p className="text-gray-400 max-w-2xl mx-auto">
Experience adaptive quizzes with AI-powered questions and real-time difficulty adjustment
</p>
</div>
{/* Tab Navigation */}
<div className="flex justify-center space-x-1 mb-8">
{[
{ id: 'rooms', label: 'Live Quiz Rooms', icon: Users, description: 'Join or host live quizzes' },
{ id: 'adaptive', label: 'Adaptive Quiz', icon: Brain, description: 'AI-powered adaptive difficulty' },
{ id: 'traditional', label: 'Traditional Quizzes', icon: Target, description: 'Fixed question sets' }
].map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`px-6 py-3 rounded-lg flex items-center space-x-2 transition-colors ${
activeTab === tab.id
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
<tab.icon className="h-5 w-5" />
<div className="text-left">
<div className="font-semibold">{tab.label}</div>
<div className="text-xs opacity-75">{tab.description}</div>
</div>
</button>
))}
</div>
{/* Live Quiz Rooms Tab */}
{activeTab === 'rooms' && (
<div>
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row gap-4 mb-8 justify-center items-center">
<button
onClick={() => router.push('/quiz-host')}
className="bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 px-6 py-3 rounded-lg font-semibold flex items-center space-x-2 transition-colors"
>
<Crown className="h-5 w-5" />
<span>👑 Host a Quiz</span>
</button>
<button
onClick={() => router.push('/quiz-join')}
className="bg-green-600 hover:bg-green-700 px-6 py-3 rounded-lg font-semibold flex items-center space-x-2"
>
<Users className="h-5 w-5" />
<span>🎯 Join Quiz</span>
</button>
</div>
{/* Public Rooms Grid */}
<div className="mb-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-2xl font-bold flex items-center space-x-2">
<Globe className="h-6 w-6 text-green-400" />
<span>🌍 Public Quiz Rooms</span>
</h2>
<button
onClick={fetchPublicRooms}
className="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded flex items-center space-x-2"
>
<span>🔄 Refresh</span>
</button>
</div>
{loading ? (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p>Loading rooms...</p>
</div>
) : publicRooms.length === 0 ? (
<div className="text-center py-12 bg-gray-800 rounded-lg">
<Globe className="h-16 w-16 text-gray-600 mx-auto mb-4" />
<h3 className="text-xl font-semibold mb-2">No Public Rooms Available</h3>
<p className="text-gray-400 mb-6">
Be the first to create a public quiz room!
</p>
<button
onClick={() => router.push('/quiz-host')}
className="bg-purple-600 hover:bg-purple-700 px-6 py-3 rounded-lg font-semibold"
>
🚀 Create Room
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{publicRooms.map((room) => (
<div
key={room.room_id}
className="bg-gray-800 rounded-lg p-6 hover:bg-gray-750 transition-colors border border-gray-700"
>
{/* Room Header */}
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<h3 className="text-lg font-semibold flex items-center space-x-2">
<Globe className="h-5 w-5 text-green-400" />
<span>{room.title}</span>
</h3>
<p className="text-gray-400 text-sm">Host: {room.host_name}</p>
</div>
<span className={`px-2 py-1 rounded text-xs font-medium ${getStatusColor(room.status)}`}>
{room.status}
</span>
</div>
{/* Room Stats */}
<div className="grid grid-cols-2 gap-4 mb-4 text-sm">
<div className="bg-gray-700 p-3 rounded text-center">
<div className="font-bold text-blue-400">{room.participants_count}</div>
<div className="text-gray-400">Participants</div>
</div>
<div className="bg-gray-700 p-3 rounded text-center">
<div className="font-bold text-purple-400">{room.questions_count}</div>
<div className="text-gray-400">Questions</div>
</div>
</div>
{/* Difficulty Breakdown */}
<div className="flex justify-between text-xs mb-4">
<span className="text-green-400">Easy: {room.questions_by_difficulty?.easy || 0}</span>
<span className="text-yellow-400">Medium: {room.questions_by_difficulty?.medium || 0}</span>
<span className="text-red-400">Hard: {room.questions_by_difficulty?.hard || 0}</span>
</div>
{/* Room Code */}
<div className="text-center mb-4">
<span className="bg-gray-700 px-3 py-1 rounded font-mono text-blue-400">
Code: {room.room_code}
</span>
</div>
{/* Join Button */}
<button
onClick={() => router.push(`/quiz-join?room=${room.room_code}`)}
className="w-full bg-green-600 hover:bg-green-700 p-3 rounded font-semibold flex items-center justify-center space-x-2"
>
<Play className="h-4 w-4" />
<span>Join Room</span>
</button>
</div>
))}
</div>
)}
</div>
</div>
)}
{/* Adaptive Quiz Tab */}
{activeTab === 'adaptive' && (
<div className="text-center">
<div className="max-w-2xl mx-auto mb-8">
<Brain className="h-16 w-16 text-purple-400 mx-auto mb-4" />
<h2 className="text-3xl font-bold mb-4">🧠 Adaptive AI Quiz</h2>
<p className="text-gray-400 mb-6">
Experience an intelligent quiz that adapts to your skill level in real-time using our trained CNN model.
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className="bg-gray-800 p-4 rounded-lg">
<Target className="h-8 w-8 text-blue-400 mx-auto mb-2" />
<h3 className="font-semibold mb-1">Adaptive Difficulty</h3>
<p className="text-sm text-gray-400">
Questions adjust based on your performance
</p>
</div>
<div className="bg-gray-800 p-4 rounded-lg">
<Brain className="h-8 w-8 text-purple-400 mx-auto mb-2" />
<h3 className="font-semibold mb-1">AI Predictions</h3>
<p className="text-sm text-gray-400">
See how our AI model would answer
</p>
</div>
<div className="bg-gray-800 p-4 rounded-lg">
<Sparkles className="h-8 w-8 text-green-400 mx-auto mb-2" />
<h3 className="font-semibold mb-1">Smart Analytics</h3>
<p className="text-sm text-gray-400">
Track performance across difficulty levels
</p>
</div>
</div>
<button
onClick={() => router.push('/adaptive-quiz')}
className="bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 px-8 py-4 rounded-lg font-semibold flex items-center justify-center space-x-2 mx-auto"
>
<Sparkles className="h-5 w-5" />
<span>🚀 Start Adaptive Quiz</span>
</button>
</div>
</div>
)}
{/* Traditional Quizzes Tab */}
{activeTab === 'traditional' && (
<div>
{/* AI Status & Create Buttons */}
<div className="flex flex-col sm:flex-row gap-4 mb-8 justify-center items-center">
{aiAvailable && (
<button
onClick={() => router.push('/quizzes/generate')}
className="bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 px-6 py-3 rounded-lg font-semibold flex items-center space-x-2 transition-colors"
>
<Brain className="h-5 w-5" />
<Sparkles className="h-4 w-4" />
<span>🚀 Generate AI Quiz</span>
</button>
)}
<button
onClick={() => router.push('/quizzes/create')}
className="bg-green-600 hover:bg-green-700 px-6 py-3 rounded-lg font-semibold flex items-center space-x-2"
>
<Plus className="h-5 w-5" />
<span>Create Manual Quiz</span>
</button>
</div>
{/* AI Status Banner */}
{aiAvailable && (
<div className="bg-gradient-to-r from-purple-900 to-blue-900 border border-purple-600 p-4 rounded-lg mb-8">
<div className="flex items-center space-x-3">
<Brain className="h-6 w-6 text-purple-400" />
<div>
<h3 className="font-semibold">🤖 AI Service Active</h3>
<p className="text-sm text-gray-300">
Our trained CNN model is ready to generate intelligent quizzes and provide feedback
</p>
</div>
</div>
</div>
)}
{/* Traditional Quizzes Grid */}
{quizzes.length === 0 ? (
<div className="text-center py-12">
<Brain className="h-16 w-16 text-gray-600 mx-auto mb-4" />
<h3 className="text-xl font-semibold mb-2">No Traditional Quizzes Yet</h3>
<p className="text-gray-400 mb-6">
Create your first quiz or generate one using AI
</p>
{aiAvailable && (
<button
onClick={() => router.push('/quizzes/generate')}
className="bg-purple-600 hover:bg-purple-700 px-6 py-3 rounded-lg font-semibold"
>
🚀 Generate AI Quiz
</button>
)}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{quizzes.map((quiz) => (
<div
key={quiz._id}
className="bg-gray-800 rounded-lg p-6 hover:bg-gray-750 transition-colors cursor-pointer"
onClick={() => router.push(`/quizzes/${quiz.id}`)}
>
{/* Quiz Header */}
<div className="flex items-start justify-between mb-4">
<h3 className="text-lg font-semibold flex items-center space-x-2">
{quiz.generated_by === 'AI' && (
<Brain className="h-5 w-5 text-purple-400" />
)}
<span>{quiz.title}</span>
</h3>
<span className={`px-2 py-1 rounded text-xs font-medium ${getDifficultyColor(quiz.difficulty)}`}>
{quiz.difficulty}
</span>
</div>
{/* Description */}
<p className="text-gray-400 text-sm mb-4 line-clamp-2">
{quiz.description}
</p>
{/* Stats */}
<div className="flex items-center justify-between text-sm text-gray-500">
<div className="flex items-center space-x-4">
<span className="flex items-center space-x-1">
<Users className="h-4 w-4" />
<span>{quiz.questions?.length || 0} questions</span>
</span>
<span className="flex items-center space-x-1">
<Trophy className="h-4 w-4" />
<span>{quiz.total_points} pts</span>
</span>
</div>
{quiz.generated_by === 'AI' && (
<div className="flex items-center space-x-1 text-purple-400">
<Sparkles className="h-3 w-3" />
<span className="text-xs">AI Generated</span>
</div>
)}
</div>
{/* Date */}
<div className="mt-3 pt-3 border-t border-gray-700">
<span className="text-xs text-gray-500">
Created {new Date(quiz.created_at).toLocaleDateString()}
</span>
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
</div>
)
} }
+1 -1
View File
@@ -38,7 +38,6 @@
"@radix-ui/react-toggle": "1.1.1", "@radix-ui/react-toggle": "1.1.1",
"@radix-ui/react-toggle-group": "1.1.1", "@radix-ui/react-toggle-group": "1.1.1",
"@radix-ui/react-tooltip": "1.1.6", "@radix-ui/react-tooltip": "1.1.6",
"autoprefixer": "^10.4.20",
"axios": "latest", "axios": "latest",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -70,6 +69,7 @@
"@types/node": "^22", "@types/node": "^22",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"autoprefixer": "^10.4.20",
"postcss": "^8.5", "postcss": "^8.5",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "^5" "typescript": "^5"
+3 -3
View File
@@ -95,9 +95,6 @@ importers:
'@radix-ui/react-tooltip': '@radix-ui/react-tooltip':
specifier: 1.1.6 specifier: 1.1.6
version: 1.1.6(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 1.1.6(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
autoprefixer:
specifier: ^10.4.20
version: 10.4.21(postcss@8.5.6)
axios: axios:
specifier: latest specifier: latest
version: 1.11.0 version: 1.11.0
@@ -186,6 +183,9 @@ importers:
'@types/react-dom': '@types/react-dom':
specifier: ^19 specifier: ^19
version: 19.1.6(@types/react@19.1.8) version: 19.1.6(@types/react@19.1.8)
autoprefixer:
specifier: ^10.4.20
version: 10.4.21(postcss@8.5.6)
postcss: postcss:
specifier: ^8.5 specifier: ^8.5
version: 8.5.6 version: 8.5.6
+43
View File
@@ -1,6 +1,7 @@
# Python 3.10 compatible versions # Python 3.10 compatible versions
# Core ML and Data Science Libraries # Core ML and Data Science Libraries
<<<<<<< HEAD
tensorflow>=2.18.0 tensorflow>=2.18.0
keras>=3.8.0 keras>=3.8.0
numpy>=1.26.0 numpy>=1.26.0
@@ -57,6 +58,48 @@ python-dateutil>=2.9.0
scipy>=1.16.0 scipy>=1.16.0
joblib>=1.5.1 joblib>=1.5.1
threadpoolctl>=3.6.0 threadpoolctl>=3.6.0
=======
tensorflow>=2.15.0,<2.18.0
keras>=2.15.0,<3.0.0
numpy>=1.24.0,<1.26.0
pandas>=2.0.0,<2.2.0
scikit-learn>=1.3.0,<1.6.0
# Text Processing
tensorflow-io-gcs-filesystem>=0.31.0
# Visualization
matplotlib>=3.7.0,<3.10.0
seaborn>=0.12.0,<0.13.0
# Data Handling
tqdm>=4.65.0
# Optional: If you want to download data from Kaggle
kaggle>=1.5.12
# Additional dependencies
h5py>=3.8.0
protobuf>=4.21.0,<5.0.0
absl-py>=1.4.0
astunparse>=1.6.3
gast>=0.4.0
google-pasta>=0.2.0
opt-einsum>=3.3.0
termcolor>=2.1.0
typing-extensions>=4.5.0
wrapt>=1.14.0
grpcio>=1.48.0,<2.0.0
tensorboard>=2.15.0,<2.18.0
# Date and Time
python-dateutil>=2.8.2
# For regularization and advanced features
scipy>=1.10.0,<1.16.0 # ← Fixed for Python 3.10
joblib>=1.2.0
threadpoolctl>=3.1.0
>>>>>>> 022bc42 (qizz + panel)
# Web frameworks # Web frameworks
fastapi==0.104.1 fastapi==0.104.1