| import os |
| import json |
| from flask import Flask, session, request, redirect, url_for, render_template_string, flash, jsonify |
| from openai import OpenAI |
| from database import init_db, register_user, authenticate_user, get_db_connection |
|
|
| app = Flask(__name__) |
| |
| app.secret_key = "examforge_nigerian_cbt_secure_secret" |
|
|
| |
| init_db() |
|
|
| |
| BASE_LAYOUT = """ |
| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>{{ title }} | ExamForge CBT</title> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Space+Grotesk:wght@500;700&display=swap" rel="stylesheet"> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script> |
| tailwind.config = { |
| theme: { |
| extend: { |
| fontFamily: { |
| sans: ['Inter', 'sans-serif'], |
| display: ['Space Grotesk', 'sans-serif'], |
| }, |
| colors: { |
| primary: '#2563eb', |
| 'primary-dark': '#1d4ed8', |
| 'secondary': '#0ea5e9', |
| 'dark-navy': '#0f172a', |
| } |
| } |
| } |
| } |
| </script> |
| <style> |
| body { |
| font-family: 'Inter', sans-serif; |
| background-color: #f8fafc; |
| background-image: radial-gradient(at 0% 0%, rgba(37, 99, 235, 0.05) 0px, transparent 50%), |
| radial-gradient(at 100% 100%, rgba(14, 165, 233, 0.05) 0px, transparent 50%); |
| } |
| .glass-card { |
| background: rgba(255, 255, 255, 0.8); |
| backdrop-filter: blur(24px); |
| -webkit-backdrop-filter: blur(24px); |
| border: 1px solid rgba(255, 255, 255, 0.7); |
| } |
| </style> |
| </head> |
| <body class="min-h-screen flex flex-col justify-between"> |
| |
| <!-- Header Navigation --> |
| <header class="py-4 px-6 md:px-12 flex justify-between items-center bg-white/50 backdrop-blur-md border-b border-gray-100/80 z-50"> |
| <a href="/" class="flex items-center gap-2.5"> |
| <div class="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center shadow-lg shadow-blue-500/20"> |
| <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path> |
| </svg> |
| </div> |
| <span class="font-display font-bold text-xl text-dark-navy tracking-tight">ExamForge <span class="text-blue-600">CBT</span></span> |
| </a> |
| |
| <div class="flex items-center gap-4"> |
| {% if session.get('user_id') %} |
| <span class="text-[10px] font-extrabold bg-blue-50 text-blue-700 px-3 py-1.5 rounded-full border border-blue-100 uppercase tracking-widest"> |
| {{ session.get('role').upper() }} |
| </span> |
| <a href="/logout" class="text-xs font-bold text-red-600 hover:text-red-700 hover:underline transition">Log Out</a> |
| {% else %} |
| <span class="text-xs font-semibold text-gray-400 flex items-center gap-1.5"> |
| <span class="w-2.5 h-2.5 bg-green-500 rounded-full animate-pulse"></span> Offline Prototype |
| </span> |
| {% endif %} |
| </div> |
| </header> |
| |
| <!-- Main Container --> |
| <main class="flex-grow flex items-center justify-center p-4"> |
| {% block content %}{% endblock %} |
| </main> |
| |
| <!-- Secure Footer Status --> |
| <footer class="py-4 text-center border-t border-gray-200/40 bg-white/25 backdrop-blur-sm"> |
| <p class="text-[10px] font-bold text-gray-400 uppercase tracking-[0.2em] flex items-center justify-center gap-1.5"> |
| <svg class="w-3.5 h-3.5 text-gray-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path> |
| </svg> |
| <span>NIGERIAN INSTITUTIONAL CBT SERVER</span> |
| <span>•</span> |
| <span>SECURE LOCAL SQLITE METRICS</span> |
| </p> |
| </footer> |
| |
| </body> |
| </html> |
| """ |
|
|
| |
| LOGIN_TEMPLATE = """ |
| {% extends "base" %} |
| {% block content %} |
| <div class="w-full max-w-md my-8"> |
| |
| {% with messages = get_flashed_messages(with_categories=true) %} |
| {% if messages %} |
| {% for category, msg in messages %} |
| <div class="mb-4 p-4 rounded-xl text-xs font-bold {% if category == 'error' %}bg-red-50 text-red-600 border border-red-100{% else %}bg-green-50 text-green-700 border border-green-100{% endif %}"> |
| {{ msg }} |
| </div> |
| {% endfor %} |
| {% endif %} |
| {% endwith %} |
| |
| <div class="glass-card rounded-[2rem] overflow-hidden shadow-2xl shadow-slate-900/5"> |
| |
| <!-- Role Choosing Tabs --> |
| <div class="flex bg-slate-100/50 border-b border-gray-100 p-2"> |
| <button onclick="window.location.href='/login?role=student'" class="flex-1 py-3 text-xs font-extrabold uppercase tracking-widest rounded-xl transition {% if current_role == 'student' %}bg-white text-slate-800 shadow-sm border border-white{% else %}text-gray-400 hover:text-slate-800{% endif %}"> |
| STUDENT |
| </button> |
| <button onclick="window.location.href='/login?role=teacher'" class="flex-1 py-3 text-xs font-extrabold uppercase tracking-widest rounded-xl transition {% if current_role == 'teacher' %}bg-white text-slate-800 shadow-sm border border-white{% else %}text-gray-400 hover:text-slate-800{% endif %}"> |
| TEACHER |
| </button> |
| </div> |
| |
| <div class="p-8 md:p-10"> |
| <div class="text-center mb-8"> |
| <h1 class="text-3xl font-display font-extrabold text-slate-800 mb-2"> |
| {% if current_role == 'student' %}Student Login{% else %}Teacher Access{% endif %} |
| </h1> |
| <p class="text-xs text-gray-500 font-medium"> |
| {% if current_role == 'student' %} |
| Input custom ID format: <strong>STU-[CLASS]-[NAME]-[NUM]</strong> |
| {% else %} |
| Input custom ID format: <strong>TCH-[SUBJECT]-[NAME]-[NUM]</strong> |
| {% endif %} |
| </p> |
| </div> |
| |
| <form action="/" method="POST" class="space-y-6"> |
| <!-- Submit validation values --> |
| <input type="hidden" name="role" value="{{ current_role }}"> |
| |
| <div class="space-y-2"> |
| <label class="text-[10px] font-bold text-gray-400 uppercase tracking-widest block px-1">Custom Account ID</label> |
| <input type="text" name="user_id" required |
| value="{{ prefilled_id }}" |
| placeholder="{% if current_role == 'student' %}e.g. STU-SS1-ADM-001{% else %}e.g. TCH-MTH-BI-001{% endif %}" |
| class="w-full px-5 py-4 bg-white border border-gray-200 rounded-xl focus:border-blue-500 focus:bg-white focus:ring-4 focus:ring-blue-500/5 outline-none transition font-mono font-bold text-slate-700"> |
| </div> |
| |
| <div class="space-y-2"> |
| <label class="text-[10px] font-bold text-gray-400 uppercase tracking-widest block px-1">Security PIN / Numeric Password</label> |
| <input type="password" name="pin" pattern="\\d{4}" maxlength="4" required placeholder="••••" |
| class="w-full px-5 py-4 bg-white border border-gray-200 rounded-xl focus:border-blue-500 focus:bg-white focus:ring-4 focus:ring-blue-500/5 outline-none transition font-mono font-black text-center text-xl tracking-[1.2em] text-slate-800"> |
| </div> |
| |
| <button type="submit" class="w-full py-4 bg-blue-600 text-white rounded-xl font-bold text-sm tracking-wide hover:bg-blue-700 transition shadow-lg shadow-blue-500/10 flex items-center justify-center gap-1.5 cursor-pointer"> |
| Proceed to Dashboard |
| <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path> |
| </svg> |
| </button> |
| </form> |
| |
| <div class="mt-8 pt-6 border-t border-gray-200/60 text-center"> |
| <p class="text-xs text-gray-500"> |
| Need an offline CBT profile? |
| <a href="/register?role={{ current_role }}" class="text-blue-600 font-bold hover:underline">Register Profile</a> |
| </p> |
| </div> |
| </div> |
| </div> |
| </div> |
| {% endblock %} |
| """ |
|
|
| |
| REGISTER_TEMPLATE = """ |
| {% extends "base" %} |
| {% block content %} |
| <div class="w-full max-w-lg my-8"> |
| |
| {% with messages = get_flashed_messages(with_categories=true) %} |
| {% if messages %} |
| {% for category, msg in messages %} |
| <div class="mb-4 p-4 rounded-xl text-xs font-bold bg-red-50 text-red-600 border border-red-100"> |
| {{ msg }} |
| </div> |
| {% endfor %} |
| {% endif %} |
| {% endwith %} |
| |
| <div class="glass-card rounded-[2rem] overflow-hidden shadow-2xl shadow-slate-900/5"> |
| <div class="p-8 md:p-10"> |
| <div class="mb-8"> |
| <div class="flex justify-between items-center mb-2"> |
| <span class="text-[10px] font-extrabold uppercase tracking-widest bg-blue-50 text-blue-600 border border-blue-100 px-3 py-1 rounded-full"> |
| {{ current_role }} enrollment registry |
| </span> |
| <a href="/login?role={{ current_role }}" class="text-xs font-semibold text-gray-500 hover:text-blue-600 flex items-center gap-1"> |
| <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5"></path> |
| </svg> Back to login |
| </a> |
| </div> |
| <h1 class="text-3xl font-display font-extrabold text-slate-800">CBT Profile Registry</h1> |
| </div> |
| |
| <form action="/register" method="POST" class="space-y-5"> |
| <input type="hidden" name="role" value="{{ current_role }}"> |
| |
| <div class="space-y-2"> |
| <label class="text-[10px] font-bold text-gray-400 uppercase tracking-widest block px-1">Full Name</label> |
| <input type="text" name="full_name" required placeholder="e.g. Sandra Alabi" |
| class="w-full px-5 py-4 bg-white border border-gray-200 rounded-xl focus:border-blue-500 focus:bg-white focus:ring-4 focus:ring-blue-500/5 outline-none transition font-semibold text-slate-700"> |
| </div> |
| |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| {% if current_role == 'student' %} |
| <div class="space-y-2"> |
| <label class="text-[10px] font-bold text-gray-400 uppercase tracking-widest block px-1">Classroom</label> |
| <select name="class_or_subject" class="w-full px-5 py-4 bg-white border border-gray-200 rounded-xl focus:border-blue-500 focus:bg-white outline-none transition font-extrabold text-slate-700"> |
| <option value="SS1">Senior Secondary 1 (SS1)</option> |
| <option value="SS2">Senior Secondary 2 (SS2)</option> |
| <option value="SS3">Senior Secondary 3 (SS3)</option> |
| <option value="JSS1">Junior Secondary 1 (JSS1)</option> |
| <option value="JSS2">Junior Secondary 2 (JSS2)</option> |
| <option value="JSS3">Junior Secondary 3 (JSS3)</option> |
| </select> |
| </div> |
| {% else %} |
| <div class="space-y-2"> |
| <label class="text-[10px] font-bold text-gray-400 uppercase tracking-widest block px-1">Specialty Subject</label> |
| <select name="class_or_subject" class="w-full px-5 py-4 bg-white border border-gray-200 rounded-xl focus:border-blue-500 focus:bg-white outline-none transition font-extrabold text-slate-700"> |
| <option value="Mathematics">Mathematics</option> |
| <option value="English">English</option> |
| <option value="Physics">Physics</option> |
| <option value="Chemistry">Chemistry</option> |
| <option value="Biology">Biology</option> |
| <option value="ICT">ICT</option> |
| </select> |
| </div> |
| {% endif %} |
| |
| <div class="space-y-2"> |
| <label class="text-[10px] font-bold text-gray-400 uppercase tracking-widest block px-1">4-Digit Security PIN</label> |
| <input type="password" name="pin" pattern="\\d{4}" maxlength="4" required placeholder="e.g. 1957" |
| class="w-full px-5 py-4 bg-white border border-gray-200 rounded-xl focus:border-blue-500 focus:bg-white focus:ring-4 focus:ring-blue-500/5 outline-none transition font-mono font-black text-center text-xl tracking-[1em] text-slate-800"> |
| </div> |
| </div> |
| |
| <div class="bg-blue-50/50 border border-blue-100 p-4 rounded-xl"> |
| <p class="text-xs text-blue-700 leading-relaxed font-medium"> |
| <strong>Nigeria CBT Coding Engine:</strong> The backend will instantly secure your educational records and automatically generate your official ID starting with <strong>{{ current_role.upper()[:3] }}-</strong>. |
| </p> |
| </div> |
| |
| <button type="submit" class="w-full py-4 bg-blue-600 hover:bg-blue-700 text-white rounded-xl font-bold text-sm tracking-wide transition shadow-lg shadow-blue-500/10 cursor-pointer"> |
| Enroll Profile inside SQLite |
| </button> |
| </form> |
| </div> |
| </div> |
| </div> |
| {% endblock %} |
| """ |
|
|
| |
| REGISTER_SUCCESS_TEMPLATE = """ |
| {% extends "base" %} |
| {% block content %} |
| <div class="w-full max-w-md my-8"> |
| <div class="glass-card rounded-[2rem] p-8 md:p-12 text-center shadow-2xl shadow-slate-900/5"> |
| |
| <div class="w-16 h-16 bg-green-50 rounded-full flex items-center justify-center mx-auto mb-6 border border-green-100"> |
| <svg class="w-8 h-8 text-green-600" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path> |
| </svg> |
| </div> |
| |
| <h1 class="text-3xl font-display font-extrabold text-slate-800 mb-2">Registered!</h1> |
| <p class="text-xs text-gray-500 mb-8 font-medium">Account registered successfully inside secure local SQLite database.</p> |
| |
| <div class="bg-blue-50 border-2 border-dashed border-blue-200 p-6 rounded-2xl mb-6"> |
| <span class="text-[9px] font-extrabold text-blue-500 uppercase tracking-widest block mb-2">Assigned Academic CBT ID</span> |
| <span class="text-2xl font-mono font-extrabold text-blue-700 tracking-wider block">{{ user_id }}</span> |
| </div> |
| |
| <div class="p-4 bg-amber-50 rounded-xl mb-8 border border-amber-100 text-left"> |
| <p class="text-xs text-amber-800 leading-relaxed font-semibold flex gap-2"> |
| <svg class="w-5 h-5 text-amber-600 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path> |
| </svg> |
| <span>Write this ID down or copy it! It is required to log in along with your 4-digit PIN password.</span> |
| </p> |
| </div> |
| |
| <a href="/login?id={{ user_id }}&role={{ role }}" class="inline-flex w-full py-4 bg-slate-900 hover:bg-slate-850 text-white rounded-xl font-bold text-sm transition justify-center items-center gap-1.5 cursor-pointer"> |
| Proceed with Login |
| <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path> |
| </svg> |
| </a> |
| </div> |
| </div> |
| {% endblock %} |
| """ |
|
|
| |
| TEACHER_TEMPLATE = """ |
| {% extends "base" %} |
| {% block content %} |
| <div class="w-full max-w-4xl my-8"> |
| |
| <div class="bg-gradient-to-r from-blue-600 to-indigo-600 text-white rounded-3xl p-6 md:p-8 shadow-xl shadow-blue-500/15 mb-8 flex flex-col md:flex-row justify-between items-start md:items-center gap-6"> |
| <div> |
| <span class="text-[10px] font-extrabold uppercase tracking-widest bg-white/15 px-3 py-1 rounded-full border border-white/10 mb-3 inline-block"> |
| CBT TEACHER CONSOLE |
| </span> |
| <h1 class="text-3xl font-display font-extrabold tracking-tight mb-2">Welcome Back, {{ user.full_name }}</h1> |
| <p class="text-sm text-blue-100 font-medium"> |
| Admin Custom ID: <span class="font-bold underline tracking-wider">{{ user.id }}</span> |
| </p> |
| </div> |
| <div class="bg-white/10 backdrop-blur-md border border-white/20 p-4 rounded-2xl w-full md:w-auto"> |
| <p class="text-[9px] font-bold text-blue-200 uppercase tracking-wider mb-1">Designated Specialty</p> |
| <p class="text-xl font-extrabold tracking-tight">{{ user.class_or_subject }}</p> |
| </div> |
| </div> |
| |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> |
| |
| <!-- SIDEBAR FOR QUESTION METRICS --> |
| <div class="lg:col-span-1 space-y-6"> |
| <div class="glass-card rounded-2xl p-6 shadow-sm border border-white/60"> |
| <h3 class="text-xs font-extrabold text-slate-400 uppercase tracking-widest mb-4">CBT Database Status</h3> |
| <div class="space-y-4"> |
| <div class="flex justify-between py-2 border-b border-gray-100 text-sm"> |
| <span class="text-gray-500 font-medium">Role Level:</span> |
| <span class="text-slate-800 font-bold">Subject Master</span> |
| </div> |
| <div class="flex justify-between py-2 border-b border-gray-100 text-sm"> |
| <span class="text-gray-500 font-medium">Topic Focus:</span> |
| <span class="text-slate-800 font-bold max-w-[140px] text-right truncate"> |
| {% if session.get('exam_topic') %}{{ session.get('exam_topic') }}{% else %}Not Generatd{% endif %} |
| </span> |
| </div> |
| <div class="flex justify-between py-2 border-b border-gray-100 text-sm"> |
| <span class="text-gray-500 font-medium">Active Cache:</span> |
| <span class="text-slate-800 font-bold"> |
| {% if session.get('active_exam') %}Yes (3 Questions){% else %}Empty Dashboard{% endif %} |
| </span> |
| </div> |
| </div> |
| </div> |
| |
| <div class="bg-slate-900 text-white rounded-2xl p-6"> |
| <h3 class="text-xs font-extrabold text-slate-400 uppercase tracking-widest mb-3">NVIDIA NIM AI CORE</h3> |
| <p class="text-xs leading-relaxed text-slate-300"> |
| This module utilizes NVIDIA cloud-accelerated Meta Llama 3.3 70B models inside security metrics. Exams generated adapt straight to West African Examination Syllabus metrics. |
| </p> |
| </div> |
| </div> |
| |
| <!-- GENERATION HUB --> |
| <div class="lg:col-span-2 space-y-6"> |
| |
| {% with messages = get_flashed_messages(with_categories=true) %} |
| {% if messages %} |
| {% for category, msg in messages %} |
| <div class="p-4 rounded-xl text-xs font-bold {% if category == 'error' %}bg-red-50 text-red-600 border border-red-100{% else %}bg-green-50 text-green-700 border border-green-100{% endif %}"> |
| {{ msg }} |
| </div> |
| {% endfor %} |
| {% endif %} |
| {% endwith %} |
| |
| <div class="glass-card rounded-3xl p-6 md:p-8"> |
| <h2 class="text-xl font-display font-bold text-slate-800 mb-6 flex items-center gap-2"> |
| <span class="p-2 bg-blue-50 text-blue-600 rounded-lg"> |
| <svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m3.75 9v6m3-3H9m1.5-12H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"></path> |
| </svg> |
| </span> |
| Rigid AI Question Generator Engine |
| </h2> |
| |
| <form action="/teacher/generate" method="POST" class="space-y-5"> |
| <div class="space-y-2"> |
| <label class="text-[10px] font-bold text-gray-400 uppercase tracking-widest block px-1">Syllabus Topic Title</label> |
| <input type="text" name="topic" required placeholder="e.g. Quadratic Equations, Photosynthesis, Organic Chemistry" |
| class="w-full px-5 py-4 bg-white border border-gray-200 rounded-xl focus:border-blue-500 focus:bg-white outline-none transition font-semibold text-slate-700"> |
| </div> |
| |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| <div class="space-y-2"> |
| <label class="text-[10px] font-bold text-gray-400 uppercase tracking-widest block px-1">School exam category</label> |
| <select name="exam_type" class="w-full px-5 py-4 bg-white border border-gray-200 rounded-xl focus:border-blue-500 focus:bg-white outline-none transition font-bold text-slate-700"> |
| <option value="WAEC">West African Senior School Certificate (WAEC)</option> |
| <option value="JAMB">Unified Tertiary Matriculation Exam (JAMB)</option> |
| <option value="Post-UTME">University Admission Screening (Post-UTME)</option> |
| </select> |
| </div> |
| |
| <div class="space-y-2"> |
| <label class="text-[10px] font-bold text-gray-400 uppercase tracking-widest block px-1">Question Volume</label> |
| <input type="text" readonly value="Exactly 3 MCQs" |
| class="w-full px-5 py-4 bg-gray-50 border border-gray-200 rounded-xl outline-none font-bold text-slate-400 cursor-not-allowed"> |
| </div> |
| </div> |
| |
| <button type="submit" class="w-full py-4 bg-blue-600 hover:bg-blue-700 text-white rounded-xl font-bold text-sm tracking-wide transition shadow-lg shadow-blue-500/10 flex items-center justify-center gap-1.5 cursor-pointer"> |
| <svg class="w-4 h-4 animate-pulse text-sky-200" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" d="M9.813 15.904L9 21l8.904-4.43m-8.904-.666a1.996 1.996 0 000 3.32m9.982-12.014l-4.432 8.904m0 0l-3.32-9.982m5.534 8.87a1.996 1.996 0 10-1.996-3.32"></path> |
| </svg> |
| Deploy Llama 3.3 NIM Generator |
| </button> |
| </form> |
| </div> |
| |
| <!-- VIEW GENERATED ITEMS --> |
| {% if session.get('active_exam') %} |
| <div class="glass-card rounded-3xl p-6 md:p-8 border border-green-150 bg-green-50/10"> |
| <div class="flex justify-between items-center mb-6"> |
| <h3 class="font-display font-extrabold text-slate-800 text-lg flex items-center gap-2"> |
| <span class="w-2.5 h-2.5 rounded-full bg-green-500"></span> |
| Active Exam Generated for Students |
| </h3> |
| <span class="text-[10px] font-extrabold px-3 py-1 bg-green-100 text-green-700 border border-green-200 rounded-full"> |
| {{ session.get('exam_type') }} CODE |
| </span> |
| </div> |
| |
| <p class="text-xs text-slate-500 font-semibold mb-6"> |
| STUDENT CODESPACE PREVIEW: Registered students logging in can now view and write this exam on their portals. |
| </p> |
| |
| <div class="space-y-6"> |
| {% for q in session.get('active_exam') %} |
| <div class="p-5 bg-white border border-gray-100 rounded-2xl shadow-sm"> |
| <div class="flex items-start gap-3"> |
| <span class="w-6 h-6 rounded-lg bg-blue-50 border border-blue-100 text-blue-600 font-bold text-xs flex items-center justify-center shrink-0"> |
| {{ loop.index }} |
| </span> |
| <div class="space-y-3 w-full"> |
| <p class="text-sm font-bold text-slate-700 leading-relaxed">{{ q.question }}</p> |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-2.5"> |
| {% for letter, val in q.options.items() %} |
| <div class="p-3 rounded-xl border border-gray-150 bg-slate-50/50 text-xs font-semibold flex justify-between"> |
| <span class="text-slate-500 uppercase">{{ letter }}: {{ val }}</span> |
| {% if letter == q.correct %} |
| <span class="text-green-600 font-bold uppercase shrink-0">✓ Correct</span> |
| {% endif %} |
| </div> |
| {% endfor %} |
| </div> |
| </div> |
| </div> |
| </div> |
| {% endfor %} |
| </div> |
| </div> |
| {% endif %} |
| |
| </div> |
| </div> |
| </div> |
| {% endblock %} |
| """ |
|
|
| |
| STUDENT_TEMPLATE = """ |
| {% extends "base" %} |
| {% block content %} |
| <div class="w-full max-w-4xl my-8"> |
| |
| <div class="bg-gradient-to-r from-blue-600 to-indigo-600 text-white rounded-3xl p-6 md:p-8 shadow-xl shadow-blue-500/15 mb-8 flex flex-col md:flex-row justify-between items-start md:items-center gap-6"> |
| <div> |
| <span class="text-[10px] font-extrabold uppercase tracking-widest bg-white/15 px-3 py-1 rounded-full border border-white/10 mb-3 inline-block"> |
| Nigeria Academic Center |
| </span> |
| <h1 class="text-3xl font-display font-extrabold tracking-tight mb-2">Welcome, {{ user.full_name }}</h1> |
| <p class="text-sm text-blue-100 font-medium"> |
| CBT Student ID: <span class="font-bold underline tracking-wider font-mono">{{ user.id }}</span> |
| </p> |
| </div> |
| <div class="bg-white/10 backdrop-blur-md border border-white/20 p-4 rounded-2xl w-full md:w-auto"> |
| <p class="text-[9px] font-bold text-blue-200 uppercase tracking-wider mb-1">Assigned Classroom</p> |
| <p class="text-xl font-extrabold tracking-tight">{{ user.class_or_subject }}</p> |
| </div> |
| </div> |
| |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> |
| |
| <!-- SIDEBAR --> |
| <div class="lg:col-span-1 space-y-6"> |
| <div class="glass-card rounded-2xl p-6 shadow-sm border border-white/60"> |
| <h3 class="text-xs font-extrabold text-slate-400 uppercase tracking-widest mb-4">Exam History</h3> |
| <div class="space-y-4"> |
| <div class="flex justify-between py-2 border-b border-gray-100 text-sm"> |
| <span class="text-gray-500 font-medium">Status Check:</span> |
| <span class="text-slate-800 font-bold">Good Standing</span> |
| </div> |
| <div class="flex justify-between py-2 border-b border-gray-100 text-sm"> |
| <span class="text-gray-500 font-medium">Class:</span> |
| <span class="text-slate-200 font-bold bg-blue-600 px-2 py-0.5 rounded text-xs">{{ user.class_or_subject }}</span> |
| </div> |
| </div> |
| </div> |
| |
| <div class="bg-slate-900 text-white rounded-2xl p-6"> |
| <h3 class="text-xs font-extrabold text-slate-400 uppercase tracking-widest mb-3">Principal Quote</h3> |
| <p class="text-xs leading-relaxed text-slate-300 italic"> |
| "Only absolute focus and dedication yields academic success. Make your parents proud and study with perfect discipline!" |
| </p> |
| </div> |
| </div> |
| |
| <!-- EXAM WRITING AREA --> |
| <div class="lg:col-span-2 space-y-6"> |
| |
| {% if not active_exam %} |
| <div class="border border-dashed border-gray-200 bg-slate-50/50 rounded-3xl p-10 text-center glass-card"> |
| <div class="w-16 h-16 bg-blue-50 text-blue-500 rounded-2xl mb-4 flex items-center justify-center mx-auto border border-blue-100"> |
| <svg class="w-7 h-7" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path> |
| </svg> |
| </div> |
| <h3 class="font-display font-extrabold text-slate-800 text-lg mb-1.5">No Active Exam Session Published</h3> |
| <p class="text-xs text-gray-500 max-w-sm mx-auto leading-relaxed"> |
| There are currently no active exams launched inside our SQLite CBT server database. Wait for your specialty subject teacher to deploy a batch of questions using their dashboard. |
| </p> |
| </div> |
| {% else %} |
| <div class="glass-card rounded-3xl p-6 md:p-8 shadow-sm"> |
| <div class="flex items-center justify-between mb-6 pb-4 border-b border-gray-100"> |
| <div> |
| <span class="text-[9px] font-extrabold uppercase px-2 py-1 bg-blue-50 text-blue-600 rounded border border-blue-100 mb-1.5 inline-block"> |
| {{ session.get('exam_type') }} CORE FOCUS |
| </span> |
| <h2 class="text-xl font-display font-bold text-slate-800"> |
| Classroom CBT: {{ session.get('exam_topic') }} |
| </h2> |
| </div> |
| <div class="text-right shrink-0"> |
| <span class="text-[10px] font-extrabold text-gray-400 uppercase tracking-widest block">Timer</span> |
| <span class="text-sm font-bold text-blue-600">Untimed Session</span> |
| </div> |
| </div> |
| |
| <!-- EXAM SESSION CBT FORM --> |
| <form action="/results" method="POST" class="space-y-8"> |
| {% for q in active_exam %} |
| <div class="space-y-4"> |
| <div class="flex items-start gap-3"> |
| <span class="w-6 h-6 rounded-lg bg-blue-50 border border-blue-100 text-blue-600 font-bold text-xs flex items-center justify-center shrink-0 mt-0.5"> |
| {{ loop.index }} |
| </span> |
| <p class="text-sm font-bold text-slate-700 leading-relaxed leading-[1.6]"> |
| {{ q.question }} |
| </p> |
| </div> |
| |
| <div class="grid grid-cols-1 gap-2.5 pl-9"> |
| {% for letter, val in q.options.items() %} |
| <label class="p-4 rounded-xl border border-gray-150 bg-slate-50/40 hover:bg-white focus-within:bg-white select-none cursor-pointer transition flex items-center gap-3"> |
| <input type="radio" required name="ans_q_{{ q.id }}" value="{{ letter }}" |
| class="w-4 h-4 text-blue-600 focus:ring-blue-500 focus:ring-2"> |
| <span class="text-xs font-semibold text-slate-600 hover:text-slate-850"> |
| <strong class="uppercase text-slate-400 mr-1.5">{{ letter }}.</strong> |
| {{ val }} |
| </span> |
| </label> |
| {% endfor %} |
| </div> |
| </div> |
| {% if not loop.last %} |
| <hr class="border-gray-100"> |
| {% endif %} |
| {% endfor %} |
| |
| <button type="submit" class="w-full py-4 bg-blue-600 hover:bg-blue-700 text-white rounded-xl font-extrabold text-sm tracking-wide transition shadow-lg shadow-blue-500/10 cursor-pointer text-center block"> |
| Finalize and Graded Evaluation |
| </button> |
| </form> |
| </div> |
| {% endif %} |
| |
| </div> |
| </div> |
| </div> |
| {% endblock %} |
| """ |
|
|
| |
| RESULTS_TEMPLATE = """ |
| {% extends "base" %} |
| {% block content %} |
| <div class="w-full max-w-2xl my-8"> |
| <div class="glass-card rounded-[2rem] overflow-hidden shadow-2xl shadow-slate-900/5"> |
| |
| <div class="p-8 md:p-10 text-center"> |
| |
| <div class="w-16 h-16 bg-blue-50 rounded-full flex items-center justify-center mx-auto mb-6 border border-blue-100"> |
| <svg class="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25"></path> |
| </svg> |
| </div> |
| |
| <h1 class="text-3xl font-display font-extrabold text-slate-800 mb-1">Exam Grading Outcome</h1> |
| <p class="text-xs text-gray-500 mb-8 font-medium">Graded evaluation score from local CBT SQLite cache</p> |
| |
| <div class="inline-flex flex-col items-center justify-center p-8 bg-blue-50/50 rounded-full w-44 h-44 mb-8 border border-blue-100 shadow-inner"> |
| <span class="text-5xl font-display font-black text-blue-600 tracking-tight">{{ score }} / 3</span> |
| <span class="text-[10px] font-extrabold text-blue-400 uppercase tracking-widest mt-1">Syllabus Score</span> |
| </div> |
| |
| <!-- AI PRINCIPAL MOTIVATIONAL EVALUATION BOX (AI CODE 2) --> |
| <div class="bg-slate-900 text-white rounded-3xl p-6 text-left mb-8 shadow-lg relative overflow-hidden"> |
| <div class="absolute -right-10 -bottom-10 opacity-5"> |
| <svg class="w-48 h-48" fill="currentColor" viewBox="0 0 24 24"> |
| <path d="M12 3c-4.97 0-9 4.03-9 9 0 2.12.74 4.07 1.97 5.61L4.35 19.4c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0l1.9-1.9C9.22 19.57 10.57 20 12 20c4.97 0 9-4.03 9-9s-4.03-9-9-9zm0 15c-3.31 0-6-2.69-6-6s2.69-6 6-6 6 2.69 6 6-2.69 6-6 6z"></path> |
| </svg> |
| </div> |
| <div class="flex items-center gap-2.5 mb-3"> |
| <span class="w-1.5 h-1.5 bg-green-500 rounded-full animate-ping"></span> |
| <h3 class="text-[10px] font-extrabold tracking-widest text-[#38bdf8] uppercase"> |
| Principal's Office evaluation Evaluation Speech |
| </h3> |
| </div> |
| <p class="text-sm text-slate-100 font-medium leading-relaxed italic font-display"> |
| "{{ speech }}" |
| </p> |
| <div class="mt-4 pt-4 border-t border-slate-800 flex justify-between items-center"> |
| <p class="text-[10px] text-slate-400 font-semibold uppercase">Principal Mrs. Victoria Omotola</p> |
| <span class="text-[8px] bg-sky-500/25 text-[#38bdf8] px-2 py-0.5 rounded uppercase font-bold tracking-wider border border-sky-400/20">Llama 3.3 Engine</span> |
| </div> |
| </div> |
| |
| <!-- LIVE RE-VALUATION EXPLAIENER PORTAL (AI CODE 3) --> |
| <div class="space-y-4 text-left"> |
| <h3 class="text-xs font-extrabold text-slate-400 uppercase tracking-widest">Question Explanations</h3> |
| |
| {% for q in exam_questions %} |
| <div class="p-5 bg-white border border-gray-100 rounded-2xl shadow-sm space-y-4"> |
| <div class="flex items-start justify-between gap-4"> |
| <div class="flex items-start gap-2"> |
| <span class="w-5 h-5 rounded-lg bg-slate-150 bg-slate-100 text-slate-600 font-bold text-[11px] flex items-center justify-center shrink-0 mt-0.5"> |
| {{ loop.index }} |
| </span> |
| <div> |
| <p class="text-xs font-bold text-slate-700 leading-relaxed">{{ q.question }}</p> |
| <p class="text-[10px] text-gray-500 font-semibold mt-1"> |
| Correct Option: <span class="text-green-600 font-extrabold">{{ q.correct }}</span> | |
| Your Choice: <span class="font-extrabold {% if user_answers[q.id|string] == q.correct %}text-green-600{% else %}text-red-500{% endif %}"> |
| {{ user_answers[q.id|string] if user_answers[q.id|string] else 'Unanswered' }} |
| </span> |
| </p> |
| </div> |
| </div> |
| </div> |
| |
| <!-- Explainer Trigger Button (Launches interactive Glassmorphism Pop-up card dynamically) --> |
| <button onclick="fetchExplanation('{{ q.id }}')" |
| class="px-3.5 py-1.5 bg-blue-50 text-blue-600 border border-blue-100 hover:bg-blue-100 rounded-lg text-xs font-bold transition flex items-center gap-1 cursor-pointer"> |
| <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z"></path> |
| </svg> |
| Ask Llama Explainer Why |
| </button> |
| |
| <!-- Hidden Explanation Holder Box --> |
| <div id="explain-box-{{ q.id }}" class="hidden p-4 bg-slate-50 border border-slate-150 rounded-xl mt-3 text-xs text-slate-600 font-medium leading-relaxed"> |
| <div class="flex items-center gap-1.5 mb-2 font-bold text-blue-600 uppercase text-[9px] tracking-wide"> |
| <span class="w-1.5 h-1.5 bg-blue-500 rounded-full"></span> Live Corrective Intelligence Explanation |
| </div> |
| <p id="explain-text-{{ q.id }}">Requesting explanation from NVIDIA NIM...</p> |
| </div> |
| </div> |
| {% endfor %} |
| </div> |
| |
| <div class="mt-8 pt-6 border-t border-gray-150 flex gap-3"> |
| <a href="/student" class="flex-1 py-4 bg-slate-900 hover:bg-slate-850 text-white font-bold text-xs rounded-xl transition text-center shadow-md"> |
| Return to Student Dashboard |
| </a> |
| <a href="/logout" class="py-4 px-6 bg-red-50 hover:bg-red-100 border border-red-100 text-red-600 font-bold text-xs rounded-xl transition text-center"> |
| Log Out Profile |
| </a> |
| </div> |
| |
| </div> |
| </div> |
| </div> |
| |
| <!-- Interactive modal routing script --> |
| <script> |
| function fetchExplanation(questionId) { |
| const box = document.getElementById('explain-box-' + questionId); |
| const textHolder = document.getElementById('explain-text-' + questionId); |
| |
| // Toggle visible state |
| if (box.classList.contains('hidden')) { |
| box.classList.remove('hidden'); |
| textHolder.innerHTML = `<span class="flex items-center gap-1 text-gray-500 font-semibold"><span class="w-3 h-3 border-2 border-blue-500 border-t-transparent animate-spin rounded-full"></span> Querying Llama 3.3 NIM API...</span>`; |
| |
| fetch('/explain/' + questionId) |
| .then(res => res.json()) |
| .then(data => { |
| if (data.explanation) { |
| textHolder.innerText = data.explanation; |
| } else { |
| textHolder.innerText = "Error: Could not retrieve secure re-valuation data."; |
| } |
| }) |
| .catch(err => { |
| textHolder.innerText = "Backend Connection Error: Verify your platform local configurations."; |
| }); |
| } else { |
| box.classList.add('hidden'); |
| } |
| } |
| </script> |
| {% endblock %} |
| """ |
|
|
|
|
| |
| def render_template(template_name, **context): |
| layout = BASE_LAYOUT |
| if template_name == "login": |
| body = LOGIN_TEMPLATE |
| elif template_name == "register": |
| body = REGISTER_TEMPLATE |
| elif template_name == "success": |
| body = REGISTER_SUCCESS_TEMPLATE |
| elif template_name == "teacher_dashboard": |
| body = TEACHER_TEMPLATE |
| elif template_name == "student_dashboard": |
| body = STUDENT_TEMPLATE |
| elif template_name == "results": |
| body = RESULTS_TEMPLATE |
| else: |
| body = "" |
| |
| combined = layout.replace("{% block content %}{% endblock %}", body) |
| return render_template_string(combined, **context) |
|
|
|
|
| |
| @app.route("/", methods=["GET", "POST"]) |
| def index(): |
| |
| if "user_id" in session: |
| if session.get("role") == "student": |
| return redirect(url_for("student_dashboard")) |
| else: |
| return redirect(url_for("teacher_dashboard")) |
|
|
| if request.method == "POST": |
| user_id = request.form.get("user_id", "").strip().upper() |
| pin = request.form.get("pin", "").strip() |
| role = request.form.get("role", "student").strip().lower() |
|
|
| |
| if len(pin) != 4 or not pin.isdigit(): |
| flash("Strict Security Reject: Password PIN must be exactly 4 numeric digits.", "error") |
| return redirect(url_for("login", role=role)) |
|
|
| |
| if not user_id.startswith("STU-") and not user_id.startswith("TCH-"): |
| flash("Rejected Identification Format: Missing STU- or TCH- prefix.", "error") |
| return redirect(url_for("login", role=role)) |
|
|
| |
| user = authenticate_user(user_id, pin) |
| if user: |
| |
| if user['role'].lower() != role: |
| flash(f"System Conflict: ID belongs to a {user['role']} profile, not {role}.", "error") |
| return redirect(url_for("login", role=role)) |
|
|
| |
| session["user_id"] = user["id"] |
| session["full_name"] = user["full_name"] |
| session["role"] = user["role"].lower() |
| session["class_or_subject"] = user["class_or_subject"] |
|
|
| if session["role"] == "student": |
| return redirect(url_for("student_dashboard")) |
| else: |
| return redirect(url_for("teacher_dashboard")) |
| else: |
| flash("Invalid Custom ID or secure Password PIN. Verify and try again.", "error") |
| return redirect(url_for("login", role=role)) |
|
|
| role = request.args.get("role", "student") |
| if role not in ["student", "teacher"]: |
| role = "student" |
| return render_template("login", title="Sign In", current_role=role, prefilled_id=request.args.get("id", "")) |
|
|
|
|
| @app.route("/login") |
| def login(): |
| role = request.args.get("role", "student") |
| if role not in ["student", "teacher"]: |
| role = "student" |
| return render_template("login", title="Sign In", current_role=role, prefilled_id=request.args.get("id", "")) |
|
|
|
|
| |
| @app.route("/register", methods=["GET", "POST"]) |
| def register(): |
| if "user_id" in session: |
| return redirect(url_for("index")) |
|
|
| if request.method == "POST": |
| full_name = request.form.get("full_name", "").strip() |
| role = request.form.get("role", "student").strip().lower() |
| class_or_subject = request.form.get("class_or_subject", "").strip() |
| pin = request.form.get("pin", "").strip() |
|
|
| if len(pin) != 4 or not pin.isdigit(): |
| flash("Error: Security Password PIN must be exactly 4 numeric digits.", "error") |
| return redirect(url_for("register", role=role)) |
|
|
| try: |
| generated_id = register_user(full_name, role, class_or_subject, pin) |
| return redirect(url_for("register_success", user_id=generated_id, role=role)) |
| except ValueError as e: |
| flash(str(e), "error") |
| return redirect(url_for("register", role=role)) |
|
|
| role = request.args.get("role", "student") |
| if role not in ["student", "teacher"]: |
| role = "student" |
| return render_template("register", title="Create Profile", current_role=role) |
|
|
|
|
| @app.route("/register/success") |
| def register_success(): |
| user_id = request.args.get("user_id") |
| role = request.args.get("role", "student") |
| if not user_id: |
| return redirect(url_for("login")) |
| return render_template("success", title="Registration Finished", user_id=user_id, role=role) |
|
|
|
|
| |
| @app.route("/teacher") |
| def teacher_dashboard(): |
| if "user_id" not in session or session.get("role") != "teacher": |
| return redirect(url_for("login", role="teacher")) |
| |
| user_info = { |
| "id": session.get("user_id"), |
| "full_name": session.get("full_name"), |
| "role": session.get("role"), |
| "class_or_subject": session.get("class_or_subject") |
| } |
| return render_template("teacher_dashboard", title="Teacher Panel", user=user_info) |
|
|
|
|
| @app.route("/teacher/generate", methods=["POST"]) |
| def generate_exam_questions(): |
| if "user_id" not in session or session.get("role") != "teacher": |
| return redirect(url_for("login", role="teacher")) |
|
|
| topic = request.form.get("topic", "").strip() |
| exam_type = request.form.get("exam_type", "WAEC").strip() |
|
|
| if not topic: |
| flash("Generation Error: You must supply a syllabus topic.", "error") |
| return redirect(url_for("teacher_dashboard")) |
|
|
| try: |
| |
| |
| client = OpenAI( |
| base_url="https://nvidia.com", |
| api_key=os.environ.get("NVIDIA_API_KEY", "PLACEHOLDER_KEY") |
| ) |
|
|
| prompt = f""" |
| Choose exactly 3 complex multiple choice questions (MCQ) on the topic "{topic}" conforming strictly to the Nigerian {exam_type} secondary syllabus metrics. |
| Return ONLY a valid, raw, unformatted JSON object. |
| Do not wrap your output in markdown ```json blocks or include any extra text outside the JSON object itself. |
| |
| The JSON format must match this model EXACTLY: |
| {{ |
| "questions": [ |
| {{ |
| "id": 1, |
| "question": "What is the primary product of light reaction during photosynthesis?", |
| "options": {{ |
| "A": "Oxygen", |
| "B": "Glucose", |
| "C": "Water", |
| "D": "Carbon dioxide" |
| }}, |
| "correct": "A" |
| }}, |
| {{ |
| "id": 2, |
| "question": "Which of the following describes the dark reaction of photosynthesis?", |
| "options": {{ |
| "A": "It requires direct sunlight", |
| "B": "It occurs inside the thylakoid membrane", |
| "C": "It is light-independent and builds sucrose", |
| "D": "It releases high-density hydrogen gas" |
| }}, |
| "correct": "C" |
| }}, |
| {{ |
| "id": 3, |
| "question": "Spirogyra cells thrive inside which ecosystem element?", |
| "options": {{ |
| "A": "Deep oceanic abyssal trenches", |
| "B": "Flowing fresh ponds or streams", |
| "C": "Arid Sahara desert dunes", |
| "D": "Deciduous tropical moist tree barks" |
| }}, |
| "correct": "B" |
| }} |
| ] |
| }} |
| """ |
|
|
| |
| response = client.chat.completions.create( |
| model="meta/llama-3.3-70b-instruct", |
| messages=[ |
| {"role": "system", "content": "You are an expert curriculum examiner generating strict Nigerian school board questions. Your outputs are always pure JSON blocks only, never including wrapping characters."}, |
| {"role": "user", "content": prompt} |
| ], |
| temperature=0.2, |
| max_tokens=1024 |
| ) |
|
|
| response_text = response.choices[0].message.content.strip() |
| |
| |
| if response_text.startswith("```"): |
| lines = response_text.splitlines() |
| if lines[0].startswith("```"): |
| lines = lines[1:] |
| if lines[-1].startswith("```"): |
| lines = lines[:-1] |
| response_text = "\n".join(lines).strip() |
|
|
| |
| parsed_data = json.loads(response_text) |
| questions_array = parsed_data.get("questions", []) |
|
|
| if len(questions_array) != 3: |
| raise ValueError("Syllabus Error: Core Model returned invalid question depth.") |
|
|
| |
| session["active_exam"] = questions_array |
| session["exam_topic"] = topic |
| session["exam_type"] = exam_type |
|
|
| flash(f"CBT Setup Confirmed! Generated 3 premium {exam_type} questions on '{topic}' successfully.", "success") |
| except Exception as e: |
| print("Llama NIM Generator Error Detail:", str(e)) |
| flash(f"API Error: Llama CBT generator failed. Please double-check your secure NVIDIA_API_KEY. (Detail: {str(e)})", "error") |
|
|
| return redirect(url_for("teacher_dashboard")) |
|
|
|
|
| |
| @app.route("/student") |
| def student_dashboard(): |
| if "user_id" not in session or session.get("role") != "student": |
| return redirect(url_for("login", role="student")) |
|
|
| user_info = { |
| "id": session.get("user_id"), |
| "full_name": session.get("full_name"), |
| "role": session.get("role"), |
| "class_or_subject": session.get("class_or_subject") |
| } |
|
|
| |
| active_exam = session.get("active_exam", None) |
|
|
| return render_template("student_dashboard", title="Student Console", user=user_info, active_exam=active_exam) |
|
|
|
|
| |
| @app.route("/results", methods=["POST"]) |
| def submit_exam_and_view_results(): |
| if "user_id" not in session or session.get("role") != "student": |
| return redirect(url_for("login", role="student")) |
|
|
| active_exam = session.get("active_exam") |
| if not active_exam: |
| flash("Error: No active exam exists to validate.", "error") |
| return redirect(url_for("student_dashboard")) |
|
|
| |
| score = 0 |
| user_answers = {} |
| for q in active_exam: |
| q_id = str(q["id"]) |
| submitted_val = request.form.get(f"ans_q_{q_id}", "").strip() |
| user_answers[q_id] = submitted_val |
| if submitted_val == q["correct"]: |
| score += 1 |
|
|
| topic = session.get("exam_topic", "General Studies") |
| |
| |
| |
| speech_output = "Weldone on completing your assessments. You must remain up and doing, let's keep working. No slacking!" |
| try: |
| client = OpenAI( |
| base_url="https://nvidia.com", |
| api_key=os.environ.get("NVIDIA_API_KEY", "PLACEHOLDER_KEY") |
| ) |
| |
| principal_prompt = f""" |
| Assess this student's outcome on an exam. |
| - Scoring grade: {score} out of 3 questions correct |
| - Topic of evaluation: "{topic}" |
| |
| Write a motivational assessment speech of EXACTLY 3 sentences. |
| You MUST roleplay as Mrs. Victoria Omotola, a strict but deeply caring secondary school Principal in Lagos, Nigeria. |
| You MUST explicitly inject authentic colloquial Nigerian administrative school idioms and words (such as "up and doing", "no slacking", "weldone", "carry first"). |
| Do not write any introductory greetings or extra sentences. Stick strictly to exactly 3 sentences. |
| """ |
|
|
| response = client.chat.completions.create( |
| model="meta/llama-3.3-70b-instruct", |
| messages=[ |
| {"role": "system", "content": "You are a Nigerian secondary school principal Mrs. Victoria Omotola with rich native style. Your output is always exactly 3 sentences of deeply encouraging and structured evaluation."}, |
| {"role": "user", "content": principal_prompt} |
| ], |
| temperature=0.7, |
| max_tokens=300 |
| ) |
| speech_output = response.choices[0].message.content.strip() |
| except Exception as e: |
| print("Mrs Omotola Evaluator Error Detail:", str(e)) |
|
|
| |
| session["user_answers"] = user_answers |
| session["last_score"] = score |
| session["principal_speech"] = speech_output |
|
|
| return render_template( |
| "results", |
| title="Graded Exam", |
| score=score, |
| speech=speech_output, |
| exam_questions=active_exam, |
| user_answers=user_answers |
| ) |
|
|
|
|
| |
| @app.route("/explain/<int:question_id>") |
| def explain_incorrect_answers(question_id): |
| if "user_id" not in session: |
| return jsonify({"error": "Unauthorized"}), 401 |
|
|
| active_exam = session.get("active_exam") |
| user_answers = session.get("user_answers", {}) |
|
|
| if not active_exam: |
| return jsonify({"explanation": "Syllabus active exam expired."}), 404 |
|
|
| |
| target_q = None |
| for q in active_exam: |
| if q["id"] == question_id: |
| target_q = q |
| break |
|
|
| if not target_q: |
| return jsonify({"explanation": "Question target not found inside workspace cache."}), 404 |
|
|
| correct_option = target_q["correct"] |
| correct_desc = target_q["options"].get(correct_option, "") |
| user_selection = user_answers.get(str(question_id), "None") |
| user_desc = target_q["options"].get(user_selection, "No Option Chosen") |
|
|
| explanation_text = "NVIDIA NIM Llama explanation loading..." |
| try: |
| client = OpenAI( |
| base_url="https://nvidia.com", |
| api_key=os.environ.get("NVIDIA_API_KEY", "PLACEHOLDER_KEY") |
| ) |
|
|
| explain_prompt = f""" |
| Analyze this multiple choice question on the school topic "{session.get('exam_topic')}": |
| Question: "{target_q['question']}" |
| Correct answer was option "{correct_option}" which is: "{correct_desc}" |
| Student selected option "{user_selection}" which is: "{user_desc}" |
| |
| Deliver a concise, conversational explanation of exactly 2-3 sentences max. |
| Plainly explain: |
| 1. Why the correct option "{correct_option}" is scientifically or mathematically correct. |
| 2. In simple, friendly Nigerian student-facing terms, why the chosen option "{user_selection}" is incorrect. |
| Keep the tone helpful and encouraging. Do not output anything other than the explanation text. |
| """ |
|
|
| response = client.chat.completions.create( |
| model="meta/llama-3.3-70b-instruct", |
| messages=[ |
| {"role": "system", "content": "You are a professional educational curriculum tutor. You explain concepts simply, concisely and directly in 3 sentences max."}, |
| {"role": "user", "content": explain_prompt} |
| ], |
| temperature=0.3, |
| max_tokens=400 |
| ) |
| explanation_text = response.choices[0].message.content.strip() |
| except Exception as e: |
| explanation_text = f"Explanation Generation Failed. Ensure your NVIDIA API metrics key is set correctly in PowerShell environment parameters. (Error: {str(e)})" |
|
|
| return jsonify({"explanation": explanation_text}) |
|
|
|
|
| |
| @app.route("/logout") |
| def logout_user(): |
| session.clear() |
| flash("Session terminated securely.", "success") |
| return redirect(url_for("login")) |
|
|
|
|
| if __name__ == "__main__": |
| print("----------------------------------------------------------------") |
| print(" EXAMFORGE CBT LOCAL LIVE FLASK COMPLIANT SERVER ") |
| print("----------------------------------------------------------------") |
| app.run(host="0.0.0.0", port=3000, debug=True) |
|
|