"""
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
{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('', 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('', 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('', 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('', 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)