""" app.py — Streamlit frontend for the Smart MCQ Solver. Run locally: streamlit run src/app.py """ import sys import os import numpy as np # ── Ensure the project root is on the Python path so `src.*` imports work ──── _SRC_DIR = os.path.dirname(os.path.abspath(__file__)) _PROJECT_ROOT = os.path.dirname(_SRC_DIR) if _PROJECT_ROOT not in sys.path: sys.path.insert(0, _PROJECT_ROOT) import streamlit as st from src.config import ( DEVICE, ID2LABEL, BAR_COLORS, EXAMPLES, MODEL_SOURCE, ) from src.model import load_roberta_model, predict_mcq # ── Page config ─────────────────────────────────────────────────────────────── st.set_page_config( page_title="Smart MCQ Solver", page_icon="🧠", layout="wide", initial_sidebar_state="expanded", ) # ── Session State Init ──────────────────────────────────────────────────────── for k in ["q_prompt", "opt_A", "opt_B", "opt_C", "opt_D", "opt_E"]: if k not in st.session_state: st.session_state[k] = "" def load_example(): """Callback to update text areas when an example is selected.""" choice = st.session_state.ex_choice_box if choice != "(none)": idx = int(choice.split()[-1]) - 1 ex = EXAMPLES[idx] st.session_state.q_prompt = ex["prompt"] st.session_state.opt_A = ex["A"] st.session_state.opt_B = ex["B"] st.session_state.opt_C = ex["C"] st.session_state.opt_D = ex["D"] st.session_state.opt_E = ex["E"] else: st.session_state.q_prompt = "" st.session_state.opt_A = "" st.session_state.opt_B = "" st.session_state.opt_C = "" st.session_state.opt_D = "" st.session_state.opt_E = "" # ── Custom CSS ──────────────────────────────────────────────────────────────── st.markdown(""" """, unsafe_allow_html=True) # ── HTML render helpers ─────────────────────────────────────────────────────── def render_prob_bars(probs: np.ndarray, highlight_idx: int) -> str: html = "" for i, p in enumerate(probs): width = max(int(p * 100), 1) color = BAR_COLORS[i] bold = "font-weight:800;" if i == highlight_idx else "font-weight:600;" label_color = color if i == highlight_idx else "#94a3b8" html += f"""
{ID2LABEL[i]}
{p*100:.1f}%
""" return html def render_top3_chips(probs: np.ndarray) -> str: top3 = np.argsort(-probs)[:3] chip_classes = ["chip chip-1", "chip chip-2", "chip chip-3"] rank_labels = ["🥇", "🥈", "🥉"] html = "" for rank, idx in enumerate(top3): html += f'{rank_labels[rank]} {ID2LABEL[idx]}  {probs[idx]*100:.1f}%' return html # ───────────────────────────────────────────────────────────────────────────── # SIDEBAR # ───────────────────────────────────────────────────────────────────────────── with st.sidebar: st.markdown("### 📋 Quick Examples") st.selectbox( "Load an example question", options=["(none)"] + [f"Example {i+1}" for i in range(len(EXAMPLES))], key="ex_choice_box", on_change=load_example, label_visibility="collapsed" ) st.divider() st.markdown("### ℹ️ About the Model") source_label = "HF Hub" if MODEL_SOURCE == "hub" else "Local" hw_label = "🟢 GPU" if DEVICE == "cuda" else "🟡 CPU" st.markdown(f"""
RoBERTa Base
Architecture
r=8
LoRA Rank
3.4MB
Adapter
{hw_label}   {DEVICE.upper()}
""", unsafe_allow_html=True) # ───────────────────────────────────────────────────────────────────────────── # MAIN PAGE # ───────────────────────────────────────────────────────────────────────────── st.markdown('
🧠 Smart MCQ Solver
', unsafe_allow_html=True) st.markdown( '
Powered by LoRA fine-tuned RoBERTa · ' 'IITM DL+GenAI T2-2026
', unsafe_allow_html=True, ) # ── Load model (cached across reruns) ───────────────────────────────────────── @st.cache_resource(show_spinner=False) def _cached_load(): return load_roberta_model() with st.spinner("⏳ Loading model weights into memory..."): try: rob_model, rob_tok = _cached_load() except Exception as err: st.error(f"❌ Failed to load model: {err}") st.stop() # ─── Two-column layout ──────────────────────────────────────────────────────── left, right = st.columns([1.1, 0.9], gap="large") # ── INPUT COLUMN ────────────────────────────────────────────────────────────── with left: st.markdown('
', unsafe_allow_html=True) st.markdown('
📝 The Question
', unsafe_allow_html=True) st.text_area( "Question / Prompt", height=110, placeholder="Enter your multiple-choice question here...", label_visibility="collapsed", key="q_prompt", ) st.markdown("
", unsafe_allow_html=True) # ── Options ─────────────────────────────────────────────────────────────── st.markdown('
', unsafe_allow_html=True) st.markdown('
🔤 Options (A–E)
', unsafe_allow_html=True) for lbl in ["A", "B", "C", "D", "E"]: st.text_area( f"Option {lbl}", height=68, placeholder=f"Type option {lbl}...", key=f"opt_{lbl}", ) st.markdown("
", unsafe_allow_html=True) predict_clicked = st.button("✨ Predict Answer", use_container_width=True) # ── RESULT COLUMN ───────────────────────────────────────────────────────────── with right: if predict_clicked: # Validation prompt = st.session_state.q_prompt.strip() opts = { "A": st.session_state.opt_A.strip(), "B": st.session_state.opt_B.strip(), "C": st.session_state.opt_C.strip(), "D": st.session_state.opt_D.strip(), "E": st.session_state.opt_E.strip(), } missing = [] if not prompt: missing.append("Question") for k, v in opts.items(): if not v: missing.append(f"Option {k}") if missing: st.error(f"⚠️ Please fill in: **{', '.join(missing)}**") st.stop() with st.spinner("🧠 Thinking..."): result = predict_mcq(prompt, opts, rob_model, rob_tok) probs = result["probs"] top1_letter = result["top1"] top1_conf = result["confidence"] * 100 # ── Top-1 prediction card ───────────────────────────────────────────── st.markdown(f"""
Top Prediction
{top1_letter}
Confidence: {top1_conf:.1f}%
""", unsafe_allow_html=True) # ── Top-3 chips ─────────────────────────────────────────────────────── st.markdown('
', unsafe_allow_html=True) st.markdown('
🏆 Top-3 Ranking
', unsafe_allow_html=True) st.markdown(f"
{render_top3_chips(probs)}
", unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) # ── Probability bars ────────────────────────────────────────────────── top1_idx = int(np.argmax(probs)) st.markdown('
', unsafe_allow_html=True) st.markdown('
📊 Detailed Probabilities
', unsafe_allow_html=True) st.markdown(f"
{render_prob_bars(probs, top1_idx)}
", unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) else: st.markdown("""
🎯
Ready for Inference
Enter your question and options on the left, then click Predict Answer.
Need a test? Load an example from the sidebar.
""", unsafe_allow_html=True) # ── Footer ──────────────────────────────────────────────────────────────────── st.markdown("""
Developed for IITM DL+GenAI T2-2026
""", unsafe_allow_html=True)