examforge / app.py
Benjahmin's picture
refactor: update authentication and ID schemes
62f971f
Raw
History Blame Contribute Delete
60.2 kB
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__)
# Secure session key
app.secret_key = "examforge_nigerian_cbt_secure_secret"
# Verify/Initialize the SQLite CBT database tables
init_db()
# BASE PREMIUM HTML/CSS LAYOUT
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 VIEW TEMPLATE
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 %}
"""
# REGISTRATION VIEW TEMPLATE
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 %}
"""
# REGISTRATION SUCCESS VIEW TEMPLATE
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 DASHBOARD VIEW TEMPLATE (RIGID QUESTION GENERATOR ENGINE)
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 DASHBOARD / EXAM INTERFACE VIEW TEMPLATE
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 / MOTIVATIONAL PRINCIPAL SPEECH VIEW TEMPLATE (LOCALIZED EVALUATION SPEECH ENGINE)
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 %}
"""
# HELPER RENDER WRAP FOR STRIP INTERFACE RENDERS
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)
# ---- CRITICAL ROUTING PIPELINE 1: INSTITUTIONAL SYSTEM LOGINS (/) ----
@app.route("/", methods=["GET", "POST"])
def index():
# If already logged in, route immediately
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()
# Enforce server-side rigid password PIN limits
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))
# Enforce structural integrity checks on ID starting values
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))
# Perform authentication inside database.py secure SQLite methods
user = authenticate_user(user_id, pin)
if user:
# Enforce that login page role matches actual db role
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))
# Store inside session variables
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", ""))
# ---- REGISTRATION CHANNELS ----
@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)
# ---- CRITICAL ROUTING PIPELINE 2: RIGID AI QUESTION GENERATOR ENGINE (/teacher) ----
@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:
# Initialize OpenAI Client pointing strictly to cloud-hosted NVIDIA NIM API Endpoint (Nvidia Nim)
# Key must remain hidden in environment
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"
}}
]
}}
"""
# Dispatch completion payload targeting our model Meta Llama-3.3-70b-instruct
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()
# Clean potential markdown wrappers if some leakage occurs
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()
# Parse generated string elements
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.")
# Cache exactly within fully resilient Flask cache session to bypass SQLite wiping on serverless deployments
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"))
# ---- STUDENT PORTAL ----
@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")
}
# Fetch active exam questions from in-memory session
active_exam = session.get("active_exam", None)
return render_template("student_dashboard", title="Student Console", user=user_info, active_exam=active_exam)
# ---- CRITICAL ROUTING PIPELINE 3: LOCALIZED PRINCIPAL EVALUATION SPEECH ENGINE (/results) ----
@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"))
# Tabulate the final score from frontend radio button submissions parameters
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")
# Fire evaluation payload to the cloud AI completion server for the localized principal speech
# Core system roleplay requirement:Mrs Victoria Victoria Omotola, Caring secondary principal.
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))
# Keep user answer details in temporary session to show review list
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
)
# ---- CRITICAL ROUTING PIPELINE 4: LIVE RE-VALUATION & ANSWER EXPLAINER API (/explain/<int:question_id>) ----
@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
# Locate the target question index
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})
# ---- LOGOUT PORTAL CHANNELS ----
@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)