mirror of
https://github.com/th30d4y/OpenLearnX.git
synced 2026-05-26 19:26:33 +00:00
qizz + panel
This commit is contained in:
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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}}
|
|
||||||
)
|
|
||||||
@@ -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()
|
|
||||||
@@ -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"
|
|
||||||
]
|
|
||||||
})
|
|
||||||
@@ -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
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -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, [])
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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": []
|
|
||||||
})
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -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'))
|
|
||||||
@@ -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
|
|
||||||
@@ -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
+185
-679
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -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
|
||||||
@@ -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
@@ -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"
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|||||||
+1038
-28
File diff suppressed because it is too large
Load Diff
@@ -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: Easy→Medium→Hard
|
||||||
|
- 1 incorrect: Hard→Medium→Easy (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']
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Generated
+3
-3
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user