study-buddy / app.py
prajvalitha's picture
Update app.py
d9fba6d verified
Raw
History Blame Contribute Delete
69 kB
"""
AI Study Helper — Professional Edition v5.0
B.Tech CSE AI Learning Assistant
Fixes applied vs previous versions:
- Model: Only meta-llama/Llama-3.1-8B-Instruct is used (Mistral variants
are NOT supported by the HF free-tier chat router for this token).
- PDF: Rewrote using fpdf2 modern API (new_x/new_y instead of ln=True).
All cell text is ASCII-safe to prevent "horizontal space" errors.
- Quiz: Fully interactive — 5 pre-created Radio slots, Submit button,
per-question correct/wrong feedback + score.
- History: DataFrame display + Clear button that actually deletes the file.
- Dropdowns: All use gr.Dropdown with interactive=True and allow_custom_value=False.
Security:
- HF_TOKEN loaded from .env only.
- Server bound to 127.0.0.1.
- All dropdown inputs validated against allow-lists.
- Free-text inputs length-capped.
- File content truncated before LLM.
- Generic error messages to users; full detail logged server-side.
- TODO(security): Add auth gate before public deployment.
- TODO(security): Rate-limit generation endpoints.
"""
import json
import logging
import os
import re
from datetime import datetime
from pathlib import Path
import gradio as gr
from dotenv import load_dotenv
from fpdf import FPDF
from fpdf.enums import XPos, YPos
from huggingface_hub import InferenceClient
try:
from pypdf import PdfReader
_PDF_OK = True
except ImportError:
_PDF_OK = False
# ── Logging ───────────────────────────────────────────────────────────────────
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)
logger = logging.getLogger(__name__)
# ── Env & Client ──────────────────────────────────────────────────────────────
load_dotenv()
_hf_token = os.getenv("HF_TOKEN")
if not _hf_token:
raise EnvironmentError(
"HF_TOKEN not set.\n"
"Add it to your .env file → https://huggingface.co/settings/tokens"
)
client = InferenceClient(api_key=_hf_token)
# Model: Llama-3.1-8B-Instruct via HF chat.completions (confirmed working)
_MODEL_PRIMARY = "meta-llama/Llama-3.1-8B-Instruct"
def _llm(prompt: str, max_tokens: int = 2048) -> str:
"""Call Llama-3.1-8B via HF chat.completions."""
try:
resp = client.chat.completions.create(
model=_MODEL_PRIMARY,
messages=[{"role": "user", "content": prompt}],
max_tokens=max_tokens,
temperature=0.7,
)
result = resp.choices[0].message.content
if result and len(result.strip()) > 20:
logger.info("Model used: %s", _MODEL_PRIMARY)
return result.strip()
except Exception as exc:
logger.error("LLM call failed: %s", exc)
return (
"[ERROR] AI is temporarily unavailable.\n\n"
"**Check:** HF_TOKEN has Read access at https://huggingface.co/settings/tokens"
)
# ── History ───────────────────────────────────────────────────────────────────
_HIST_FILE = Path("study_history.json")
_MAX_HIST = 50
def _load_hist() -> list[dict]:
if _HIST_FILE.exists():
try:
return json.loads(_HIST_FILE.read_text(encoding="utf-8"))
except Exception:
return []
return []
def _add_hist(entry: dict) -> None:
hist = _load_hist()
hist.insert(0, entry)
try:
_HIST_FILE.write_text(
json.dumps(hist[:_MAX_HIST], indent=2, ensure_ascii=False),
encoding="utf-8",
)
except Exception as exc:
logger.error("History save: %s", exc)
# ── Topics catalogue ──────────────────────────────────────────────────────────
TOPICS: dict[str, list[str]] = {
"Programming": ["Python", "Java", "C", "C++", "OOP Concepts",
"Exception Handling", "Recursion"],
"Data Structures": ["Arrays", "Linked List", "Stack", "Queue",
"Trees", "Graphs", "Hash Tables", "Heaps"],
"Algorithms": ["Sorting Algorithms", "Searching Algorithms",
"Dynamic Programming", "Greedy Algorithms",
"Divide & Conquer", "Graph Algorithms",
"Complexity Analysis"],
"Operating Systems": ["Processes & Threads", "Memory Management",
"File Systems", "CPU Scheduling",
"Deadlocks", "Virtual Memory", "Synchronization"],
"DBMS": ["Relational Model", "SQL Queries", "Normalization",
"Transactions & ACID", "Indexing & Hashing",
"ER Diagrams", "NoSQL Databases"],
"Computer Networks": ["OSI Model", "TCP/IP Protocol", "HTTP & HTTPS",
"DNS & DHCP", "Routing Algorithms",
"Socket Programming", "Network Security"],
"Artificial Intelligence": ["Search Algorithms", "Knowledge Representation",
"Expert Systems", "Game Theory",
"Planning", "Uncertainty & Probability"],
"Machine Learning": ["Linear Regression", "Decision Trees", "SVM",
"Neural Networks", "Clustering Algorithms",
"Ensemble Methods", "Feature Engineering"],
"Deep Learning": ["Neural Networks Basics", "CNNs", "RNNs & LSTMs",
"Transformers", "GANs", "Transfer Learning",
"Backpropagation"],
"Web Development": ["HTML & CSS", "JavaScript", "React",
"Node.js & Express", "REST APIs",
"Database Integration", "Authentication & JWT"],
"Cloud Computing": ["AWS Core Services", "Azure Fundamentals",
"Docker & Containers", "Kubernetes",
"Serverless Architecture", "Microservices",
"Cloud Security"],
"Cyber Security": ["Cryptography", "Network Security",
"Web Security", "Ethical Hacking",
"Blockchain", "Firewalls & IDS",
"Penetration Testing"],
}
_VALID_CATS = set(TOPICS.keys())
_VALID_TOPICS = {t for ts in TOPICS.values() for t in ts}
_VALID_LEVELS = {"Beginner", "Intermediate", "Advanced", "Easy"}
_LEVEL_CLEAN = {
"Easy": "Beginner",
"Beginner": "Beginner",
"Intermediate": "Intermediate",
"Advanced": "Advanced",
}
_VALID_MODES = {
"Learn Concept",
"Semester Exam Prep",
"Interview Preparation",
"Quick Revision",
}
_FIRST_CAT = next(iter(TOPICS))
_FIRST_TOPICS = TOPICS[_FIRST_CAT]
def update_topics(category: str) -> gr.Dropdown:
"""Refresh topic dropdown when category changes."""
if category not in _VALID_CATS:
category = _FIRST_CAT
choices = TOPICS[category]
return gr.Dropdown(choices=choices, value=choices[0], interactive=True)
# ══════════════════════════════════════════════════════════════════════════════
# AI Generation Functions
# ══════════════════════════════════════════════════════════════════════════════
def generate_notes(category: str, topic: str, level: str, mode: str) -> str:
"""Tab 1 — structured study notes."""
if category not in _VALID_CATS: return "âš ï¸ Select a valid subject."
if topic not in _VALID_TOPICS: return "âš ï¸ Select a valid topic."
if level not in _VALID_LEVELS: return "âš ï¸ Select a valid difficulty."
if mode not in _VALID_MODES: return "âš ï¸ Select a valid study mode."
clean_level = _LEVEL_CLEAN.get(level, level)
prompt = f"""You are an expert B.Tech CSE professor.
Create detailed study notes for: {category} - {topic}
Level: {clean_level} | Mode: {mode}
Structure your response with these headings:
## Explanation
Clear explanation suitable for {level} level.
## Key Concepts
6-8 essential concepts as bullet points.
## How It Works
Step-by-step mechanism.
## Real-Life Examples
2-3 practical, relatable examples.
## Code Example
Working code snippet with comments (if applicable).
## Exam Points
Most important facts for semester exams.
## Interview Q&A
5 interview questions with concise answers.
## Common Mistakes
3-5 errors students make and how to avoid them.
## Summary
4-5 line recap for quick revision."""
result = _llm(prompt)
_add_hist({"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"),
"tab": "Learn", "topic": topic, "category": category,
"mode": mode, "level": level})
return result
def generate_revision(topic: str) -> str:
"""Tab 2 — last-minute revision notes."""
topic = topic.strip()
if not topic: return "âš ï¸ Please enter a topic."
if len(topic) > 300: return "âš ï¸ Topic is too long (max 300 chars)."
prompt = f"""Create concise last-minute revision notes for B.Tech CSE exam on: {topic}
## Quick Revision: {topic}
### Must-Know Points
8-10 critical bullet points.
### Formulas / Syntax / Key Commands
Essential formulas or syntax to memorise.
### Memory Tricks
Mnemonics and shortcuts.
### Exam Checklist
Critical items to verify before the exam.
### 30-Second Summary
3-4 key lines — the absolute essence."""
result = _llm(prompt, max_tokens=1024)
_add_hist({"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"),
"tab": "Revision", "topic": topic})
return result
# ── Quiz helpers ──────────────────────────────────────────────────────────────
def _parse_quiz(text: str) -> list[dict]:
"""Extract quiz JSON from LLM output (handles various formats)."""
# Remove markdown fences
clean = re.sub(r"```(?:json)?", "", text).strip().rstrip("`").strip()
# Attempt direct parse
for candidate in (clean, text.strip()):
try:
data = json.loads(candidate)
if isinstance(data, list) and len(data) >= 3:
return data
if isinstance(data, dict):
for key in ("questions", "quiz", "mcqs", "items"):
if key in data and isinstance(data[key], list):
return data[key]
except Exception:
pass
# Find embedded JSON array
for m in re.finditer(r"\[\s*\{.*?\}\s*\]", text, re.DOTALL):
try:
data = json.loads(m.group())
if isinstance(data, list) and len(data) >= 3:
return data
except Exception:
pass
return []
def _quiz_error(msg: str) -> list:
"""Return a consistent error state for all 19 quiz outputs."""
empties = []
for _ in range(5):
empties += [
gr.update(visible=False),
gr.update(value=""),
gr.update(choices=[], value=None, visible=False),
]
return [[], msg] + empties + [gr.update(visible=False), gr.update(visible=False)]
def generate_quiz_fn(category: str, topic: str, level: str) -> list:
"""
Tab 3 — generate 5 MCQ questions.
Returns 19 updates:
quiz_state, status_md,
[group, question_md, radio] × 5,
submit_btn, results_group
"""
if (category not in _VALID_CATS
or topic not in _VALID_TOPICS
or level not in _VALID_LEVELS):
return _quiz_error("âš ï¸ Please select valid options before generating.")
clean_level = _LEVEL_CLEAN.get(level, level)
prompt = f"""Generate exactly 5 multiple choice questions about "{topic}" ({category}) for {clean_level} B.Tech CSE students.
Reply with ONLY a valid JSON array — no introduction, no markdown, no text outside the array.
[
{{
"question": "Full question text?",
"options": ["A) First option", "B) Second option", "C) Third option", "D) Fourth option"],
"correct": "A",
"explanation": "One or two sentences explaining why A is correct."
}},
{{...}},
{{...}},
{{...}},
{{...}}
]
Rules:
- Exactly 5 questions
- Each has exactly 4 options labelled A), B), C), D)
- "correct" is a single letter: A, B, C, or D
- Return ONLY the JSON array"""
raw = _llm(prompt, max_tokens=2000)
questions = _parse_quiz(raw)
if len(questions) < 3:
return _quiz_error(
"⌠Could not parse quiz from AI response. Please try again.\n\n"
f"*Raw output (debug):*\n```\n{raw[:400]}\n```"
)
# Normalise to exactly 5 questions
while len(questions) < 5:
questions.append(questions[-1])
questions = questions[:5]
status = (
f"✅ **Quiz ready!** Answer all 5 questions then click **Submit Quiz**."
)
updates = [questions, status]
for i, q in enumerate(questions):
opts = [str(o) for o in q.get("options", [])[:4]]
while len(opts) < 4:
opts.append(f"{'ABCD'[len(opts)]}) Option")
updates += [
gr.update(visible=True),
gr.update(value=f"### Q{i + 1}. {q.get('question', '')}"),
gr.update(choices=opts, value=None, visible=True, interactive=True),
]
updates += [gr.update(visible=True), gr.update(visible=False)]
_add_hist({"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"),
"tab": "Quiz", "topic": topic,
"category": category, "level": level})
return updates
def check_quiz_fn(quiz_data: list, a1, a2, a3, a4, a5) -> tuple:
"""Tab 3 — check answers and show results."""
if not quiz_data:
return gr.update(visible=False), "âš ï¸ No quiz loaded. Generate a quiz first.", ""
answers = [a1, a2, a3, a4, a5]
score = 0
blocks = []
for i, q in enumerate(quiz_data):
correct_letter = str(q.get("correct", "A")).strip().upper()
user_ans = answers[i] if i < len(answers) else None
opts = q.get("options", [])
if user_ans is None:
is_correct = False
user_display = "*(not answered)*"
else:
user_letter = str(user_ans).strip()[0].upper()
is_correct = user_letter == correct_letter
user_display = str(user_ans)
if is_correct:
score += 1
correct_text = next(
(o for o in opts if str(o).startswith(correct_letter + ")")),
f"{correct_letter}) (see study notes)",
)
verdict = "✅ Correct!" if is_correct else "⌠Wrong"
blocks.append(
f"---\n"
f"**Q{i + 1}.** {q.get('question', '')}\n\n"
f"Your answer: **{user_display}**\n\n"
f"**{verdict}**\n\n"
f"Correct answer: **{correct_text}**\n\n"
f"*{q.get('explanation', '')}*\n"
)
pct = round(score / max(len(quiz_data), 1) * 100)
grade = ("🆠Excellent!" if pct >= 80
else "👠Good job!" if pct >= 60
else "📚 Keep studying!")
score_md = f"## Score: {score}/{len(quiz_data)} ({pct}%) — {grade}"
results_md = "## Quiz Results\n\n" + "\n".join(blocks)
return gr.update(visible=True), results_md, score_md
def generate_interview(category: str, topic: str) -> str:
"""Tab 4 — placement interview preparation."""
if category not in _VALID_CATS or topic not in _VALID_TOPICS:
return "âš ï¸ Please select valid options."
prompt = f"""Prepare complete B.Tech CSE placement interview material for: {category} - {topic}
## Technical Interview Questions
### Basic Level — Q1 to Q5 with detailed answers
### Intermediate Level — Q6 to Q10 with detailed answers
### Advanced / Tricky — Q11 to Q13 with detailed answers
## Coding Problems
2-3 commonly asked problems with approach and hints.
## HR / Behavioral Questions
4-5 behavioral questions with sample answers.
## Interview Tips
Key things top interviewers look for on this topic."""
result = _llm(prompt)
_add_hist({"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"),
"tab": "Interview", "topic": topic, "category": category})
return result
def analyze_upload(file) -> str:
"""Tab 5 — summarise uploaded PDF/TXT notes."""
if file is None:
return "âš ï¸ Please upload a PDF or TXT file."
fp = Path(file.name)
text = ""
try:
if fp.suffix.lower() == ".txt":
text = fp.read_text(encoding="utf-8", errors="replace")
elif fp.suffix.lower() == ".pdf":
if not _PDF_OK:
return "âš ï¸ pypdf not installed. Run: pip install pypdf"
reader = PdfReader(str(fp))
for page in reader.pages:
t = page.extract_text()
if t:
text += t + "\n"
else:
return "âš ï¸ Unsupported format — upload a .pdf or .txt file."
except Exception as exc:
logger.error("File read: %s", exc)
return "⌠Could not read the file."
if not text.strip():
return "âš ï¸ No text extracted. The PDF may be image/scanned-based."
prompt = f"""A B.Tech CSE student uploaded their notes. Create a study guide.
---NOTES START---
{text.strip()[:4000]}
---NOTES END---
## Simple Summary
Student-friendly explanation of what these notes cover.
## Key Points
12-15 most important takeaways as bullets.
## Exam Notes
Most likely exam topics from this content.
## Practice Questions
6 questions with brief answers based on this material.
## Extra Insights
Additional context that aids understanding."""
result = _llm(prompt)
_add_hist({"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"),
"tab": "Upload", "topic": fp.name})
return result
# ── History functions ─────────────────────────────────────────────────────────
def get_history_data() -> list[list]:
"""Return history as a list of rows for gr.DataFrame."""
entries = _load_hist()
if not entries:
return []
rows = []
for i, e in enumerate(entries, 1):
topic = e.get("topic") or e.get("filename", "—")
extras = " . ".join(filter(None, [e.get("mode", ""), e.get("level", "")]))
rows.append([i, e.get("timestamp", "—"), e.get("tab", "—"),
topic, extras or "—"])
return rows
def clear_history_fn() -> tuple:
"""Delete history file and return empty table + status message."""
try:
if _HIST_FILE.exists():
_HIST_FILE.unlink()
logger.info("History cleared.")
return [], "✅ History cleared successfully!"
except Exception as exc:
logger.error("History clear: %s", exc)
return gr.update(), f"⌠Could not clear history: {exc}"
# ── PDF generation ────────────────────────────────────────────────────────────
def _safe(text: str) -> str:
"""Encode to latin-1 safely — replaces unsupported unicode chars."""
return str(text).encode("latin-1", errors="replace").decode("latin-1")
def _strip_md(text: str) -> str:
"""Remove markdown symbols that confuse plain-text PDF rendering."""
t = re.sub(r'\*{1,3}(.*?)\*{1,3}', r'\1', text) # bold/italic
t = re.sub(r'`{1,3}(.*?)`{1,3}', r'\1', t) # code
t = re.sub(r'^#{1,6}\s+', '', t, flags=re.M) # headings
t = re.sub(r'\[(.*?)\]\(.*?\)', r'\1', t) # links
t = t.replace('\x01', '') # remove stray control chars
return t
def _build_pdf(title: str, content: str, out_file: str,
topic: str = "", generated_by: str = "") -> str | None:
"""
Professional PDF export with proper text alignment and formatting.
Returns absolute path or None on failure.
"""
if not content or not content.strip():
logger.warning("PDF skipped: empty content for '%s'", title)
return None
try:
out_path = os.path.abspath(out_file)
MARGIN = 18 # mm left/right margin
LINE_H = 6.5 # body line height mm
H1_H = 10 # h1 line height
H2_H = 9 # h2 line height
H3_H = 7 # h3 line height
pdf = FPDF()
pdf.set_margins(MARGIN, MARGIN, MARGIN)
pdf.set_auto_page_break(auto=True, margin=18)
pdf.add_page()
body_w = pdf.w - 2 * MARGIN # usable text width
# ── HEADER BAND ──────────────────────────────────────────────
pdf.set_fill_color(78, 34, 180)
pdf.set_text_color(255, 255, 255)
pdf.set_font("Helvetica", "B", 18)
pdf.cell(body_w, 12, _safe("AI STUDY HELPER"),
fill=True, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align="C")
pdf.set_font("Helvetica", "B", 11)
pdf.cell(body_w, 8, _safe(title),
fill=True, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align="C")
if topic:
pdf.set_font("Helvetica", "", 9)
pdf.cell(body_w, 6, _safe(f"Topic: {topic}"),
fill=True, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align="C")
pdf.set_font("Helvetica", "I", 8)
pdf.set_text_color(220, 210, 255)
pdf.cell(body_w, 6,
_safe(f"Generated: {datetime.now().strftime('%d %b %Y at %H:%M')}"),
fill=True, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align="C")
pdf.ln(8)
# ── BODY ─────────────────────────────────────────────────────
# Sanitise: encode to latin-1 (FPDF built-in encoding), replace unknowns
safe_content = content.encode("latin-1", errors="replace").decode("latin-1")
# Track list indent state
in_code_block = False
for raw_line in safe_content.splitlines():
stripped = raw_line.strip()
# Code fence toggle — render as mono grey block
if stripped.startswith("```"):
in_code_block = not in_code_block
if in_code_block:
pdf.ln(2)
pdf.set_fill_color(240, 240, 248)
else:
pdf.set_fill_color(255, 255, 255)
pdf.ln(2)
continue
if in_code_block:
pdf.set_font("Courier", "", 9)
pdf.set_text_color(60, 30, 100)
pdf.set_fill_color(240, 240, 248)
pdf.multi_cell(body_w, 5.5, raw_line.rstrip(), fill=True,
new_x=XPos.LMARGIN, new_y=YPos.NEXT, align="L")
continue
s = stripped
if not s:
pdf.ln(3)
continue
# ── H1 (# Title) ────────────────────────────────────────
if s.startswith("# ") and not s.startswith("## "):
text = s[2:].strip()
pdf.ln(6)
pdf.set_font("Helvetica", "B", 15)
pdf.set_text_color(55, 20, 160)
pdf.multi_cell(body_w, H1_H, _safe(text),
new_x=XPos.LMARGIN, new_y=YPos.NEXT, align="L")
# Thin rule under H1
y_now = pdf.get_y()
pdf.set_draw_color(78, 34, 180)
pdf.set_line_width(0.6)
pdf.line(MARGIN, y_now + 1, MARGIN + body_w, y_now + 1)
pdf.set_line_width(0.2)
pdf.ln(5)
pdf.set_text_color(35, 35, 50)
# ── H2 (## Heading) ─────────────────────────────────────
elif s.startswith("## ") and not s.startswith("### "):
text = s[3:].strip()
pdf.ln(5)
# Left accent bar + coloured background
pdf.set_fill_color(78, 34, 180)
pdf.rect(MARGIN, pdf.get_y(), 3, H2_H, "F") # left accent bar
pdf.set_fill_color(235, 228, 255)
pdf.rect(MARGIN + 3, pdf.get_y(), body_w - 3, H2_H, "F")
pdf.set_font("Helvetica", "B", 12)
pdf.set_text_color(55, 20, 140)
# Position text after accent bar
pdf.set_x(MARGIN + 6)
pdf.cell(body_w - 6, H2_H, _safe(text),
new_x=XPos.LMARGIN, new_y=YPos.NEXT, align="L")
pdf.ln(2)
pdf.set_text_color(35, 35, 50)
# ── H3 (### Sub-heading) ────────────────────────────────
elif s.startswith("### "):
text = s[4:].strip()
pdf.ln(4)
pdf.set_font("Helvetica", "B", 11)
pdf.set_text_color(30, 110, 70)
pdf.multi_cell(body_w, H3_H, _safe(text),
new_x=XPos.LMARGIN, new_y=YPos.NEXT, align="L")
pdf.ln(1)
pdf.set_text_color(35, 35, 50)
# ── H4 (#### Sub-sub) ───────────────────────────────────
elif s.startswith("#### "):
text = s[5:].strip()
pdf.ln(3)
pdf.set_font("Helvetica", "B", 10)
pdf.set_text_color(80, 40, 160)
pdf.multi_cell(body_w, LINE_H, _safe(text),
new_x=XPos.LMARGIN, new_y=YPos.NEXT, align="L")
pdf.set_text_color(35, 35, 50)
# ── Bullet / numbered list ───────────────────────────────
elif s.startswith(("- ", "* ", "+ ")) or re.match(r'^\d+\.\s', s):
is_numbered = re.match(r'^(\d+)\.\s(.*)', s)
if is_numbered:
bullet = f"{is_numbered.group(1)}."
body_txt = is_numbered.group(2)
else:
bullet = "-"
body_txt = s[2:]
bullet = _safe(bullet.strip())
body_txt = re.sub(r'\*{1,3}(.*?)\*{1,3}', r'\1', body_txt)
body_txt = re.sub(r'`([^`]+)`', r'\1', body_txt)
body_txt = _safe(body_txt)
pdf.set_font("Helvetica", "B", 11)
pdf.set_text_color(78, 34, 180)
# Bullet symbol column (fixed 8 mm)
pdf.set_x(MARGIN + 4)
pdf.cell(8, LINE_H, bullet, new_x=XPos.RIGHT, new_y=YPos.TOP)
# Body text — wraps within remaining width
pdf.set_font("Helvetica", "", 11)
pdf.set_text_color(35, 35, 50)
text_w = body_w - 12
pdf.multi_cell(text_w, LINE_H, body_txt,
new_x=XPos.LMARGIN, new_y=YPos.NEXT, align="L")
# ── Horizontal rule ──────────────────────────────────────
elif s.startswith("---") or s.startswith("***") or s.startswith("___"):
pdf.ln(3)
pdf.set_draw_color(180, 160, 220)
pdf.set_line_width(0.4)
pdf.line(MARGIN, pdf.get_y(), MARGIN + body_w, pdf.get_y())
pdf.set_line_width(0.2)
pdf.ln(4)
# ── Normal paragraph text ────────────────────────────────
else:
pdf.set_font("Helvetica", "", 11)
pdf.set_text_color(35, 35, 50)
# Strip inline markdown formatting
display = re.sub(r'\*{1,3}(.*?)\*{1,3}', r'\1', s)
display = re.sub(r'`([^`]+)`', r'\1', display)
display = _safe(display)
pdf.multi_cell(body_w, LINE_H, display,
new_x=XPos.LMARGIN, new_y=YPos.NEXT, align="L")
# ── PAGE FOOTER ───────────────────────────────────────────────
pdf.set_y(-14)
pdf.set_font("Helvetica", "I", 8)
pdf.set_text_color(160, 155, 180)
pdf.cell(body_w / 2, 6, "AI Study Helper | B.Tech CSE", align="L")
pdf.cell(body_w / 2, 6, f"Page {pdf.page_no()}", align="R")
pdf.output(out_path)
logger.info("PDF saved: %s", out_path)
return out_path
except Exception as exc:
logger.error("PDF build failed: %s", exc)
return None
# One-liner PDF wrappers
def notes_pdf(c): return _build_pdf("Study Notes", c, "study_notes.pdf")
def revision_pdf(c): return _build_pdf("Revision Notes", c, "revision_notes.pdf")
def quiz_pdf(c): return _build_pdf("Quiz Results", c, "quiz_results.pdf")
def interview_pdf(c): return _build_pdf("Interview Prep Pack", c, "interview_prep.pdf")
def upload_pdf(c): return _build_pdf("Notes AI Summary", c, "notes_summary.pdf")
# ══════════════════════════════════════════════════════════════════════════════
# CSS — clean dark SaaS theme
# ══════════════════════════════════════════════════════════════════════════════
_CSS = """
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
/* ============================================================
BASE RESET
============================================================ */
*, *::before, *::after { box-sizing: border-box; }
body, .gradio-container, .main {
font-family: 'Inter', 'Segoe UI', system-ui, sans-serif !important;
background: #0d0d1a !important;
color: #d0d0e8 !important;
}
.gradio-container {
background:
radial-gradient(ellipse 65% 45% at 15% 8%, rgba(120,80,240,0.09) 0%, transparent 55%),
radial-gradient(ellipse 55% 40% at 85% 88%, rgba(40,170,120,0.06) 0%, transparent 50%),
#0d0d1a !important;
max-width: 1200px !important;
margin: 0 auto !important;
}
/* ============================================================
TAB NAVIGATION
============================================================ */
.tabs > .tab-nav {
background: rgba(255,255,255,0.022) !important;
border-bottom: 1px solid rgba(120,80,240,0.18) !important;
padding: 0 8px !important;
display: flex !important;
gap: 2px !important;
}
.tabs > .tab-nav > button {
color: #6060a0 !important;
font-weight: 600 !important;
font-size: 13px !important;
padding: 12px 18px !important;
border: none !important;
border-bottom: 3px solid transparent !important;
background: transparent !important;
border-radius: 0 !important;
transition: color .2s, border-color .2s, background .2s !important;
cursor: pointer !important;
white-space: nowrap !important;
}
.tabs > .tab-nav > button:hover {
color: #9070e0 !important;
background: rgba(120,80,240,0.07) !important;
}
.tabs > .tab-nav > button.selected {
color: #b090ff !important;
border-bottom: 3px solid #7f5af0 !important;
background: rgba(127,90,240,0.09) !important;
}
/* ============================================================
PANELS
============================================================ */
.block, .gr-block, .gr-box, .panel, .gr-panel,
.form, .gr-form {
background: rgba(255,255,255,0.025) !important;
border: 1px solid rgba(255,255,255,0.06) !important;
border-radius: 14px !important;
}
/* ============================================================
LABELS
============================================================ */
label > span,
.label-wrap > span,
fieldset > legend,
.gr-form label > span {
color: #8888bb !important;
font-size: 11px !important;
font-weight: 700 !important;
letter-spacing: 0.8px !important;
text-transform: uppercase !important;
}
/* ============================================================
TEXT INPUTS
============================================================ */
input[type=text], textarea {
background: rgba(8,8,22,0.90) !important;
border: 1.5px solid rgba(120,80,240,0.25) !important;
color: #e0e0f5 !important;
border-radius: 10px !important;
font-family: inherit !important;
font-size: 14px !important;
transition: border-color .2s, box-shadow .2s !important;
}
input[type=text]:focus, textarea:focus {
border-color: rgba(127,90,240,0.65) !important;
box-shadow: 0 0 0 3px rgba(127,90,240,0.12) !important;
outline: none !important;
}
/* ============================================================
RADIO BUTTONS — single-selection pill style
Only ONE button highlighted at a time (browser-enforced by
name= grouping; CSS :has() + .radio-selected JS class)
============================================================ */
.gr-radio fieldset,
.gr-radio legend {
border: none !important;
padding: 0 !important;
margin: 0 !important;
background: transparent !important;
}
/* Default pill */
.gr-radio label {
display: inline-flex !important;
align-items: center !important;
gap: 8px !important;
padding: 9px 20px !important;
margin: 4px 5px 4px 0 !important;
border: 2px solid rgba(120,80,240,0.28) !important;
border-radius: 50px !important;
background: rgba(15,10,35,0.80) !important;
color: #a090cc !important;
font-size: 14px !important;
font-weight: 600 !important;
cursor: pointer !important;
transition: background .15s ease, border-color .15s ease,
color .15s ease, transform .12s ease !important;
user-select: none !important;
white-space: nowrap !important;
}
/* Hover — never mistaken for selected */
.gr-radio label:hover {
border-color: rgba(127,90,240,0.55) !important;
background: rgba(60,30,120,0.22) !important;
color: #d0c0f0 !important;
transform: translateY(-1px) !important;
}
/* SELECTED — dark deep purple, clearly distinct, white text
Strategy A: CSS :has() (Chrome 105+, Firefox 121+, Safari 15.4+) */
.gr-radio label:has(input[type="radio"]:checked) {
background: #2d0a5c !important;
border: 2px solid #9b6cf0 !important;
color: #ffffff !important;
font-weight: 700 !important;
transform: none !important;
box-shadow:
inset 0 0 0 1px rgba(155,108,240,0.35),
0 0 18px rgba(100,40,200,0.45) !important;
}
/* Strategy B: JS-injected class .radio-selected */
.gr-radio label.radio-selected {
background: #2d0a5c !important;
border: 2px solid #9b6cf0 !important;
color: #ffffff !important;
font-weight: 700 !important;
transform: none !important;
box-shadow:
inset 0 0 0 1px rgba(155,108,240,0.35),
0 0 18px rgba(100,40,200,0.45) !important;
}
/* Force all children of selected label to white */
.gr-radio label:has(input:checked) span,
.gr-radio label:has(input:checked) p,
.gr-radio label.radio-selected span,
.gr-radio label.radio-selected p {
color: #ffffff !important;
}
/* Hide the native radio dot (the pill IS the selector) */
.gr-radio label input[type="radio"] {
accent-color: #9b6cf0 !important;
width: 14px !important;
height: 14px !important;
flex-shrink: 0 !important;
cursor: pointer !important;
}
/* ============================================================
DROPDOWNS
============================================================ */
.wrap,
[data-testid="dropdown"] > .wrap {
background: rgba(10,8,28,0.92) !important;
border: 1.5px solid rgba(120,80,240,0.28) !important;
border-radius: 11px !important;
min-height: 48px !important;
display: flex !important;
align-items: center !important;
transition: border-color .2s, box-shadow .2s !important;
}
.wrap:focus-within {
border-color: rgba(127,90,240,0.70) !important;
box-shadow: 0 0 0 3px rgba(127,90,240,0.13) !important;
}
.wrap input {
color: #ddd8ff !important;
background: transparent !important;
border: none !important;
font-size: 14px !important;
font-weight: 500 !important;
padding: 13px 16px !important;
cursor: pointer !important;
}
.wrap svg { color: #9b7ef8 !important; fill: #9b7ef8 !important; }
ul.options, .options {
background: #0f0c24 !important;
border: 1px solid rgba(127,90,240,0.40) !important;
border-radius: 11px !important;
box-shadow: 0 14px 44px rgba(0,0,0,0.65) !important;
z-index: 9999 !important;
padding: 6px !important;
margin-top: 5px !important;
max-height: 260px !important;
overflow-y: auto !important;
}
ul.options li, .options li {
color: #c4c0ee !important;
background: transparent !important;
border-radius: 8px !important;
padding: 10px 14px !important;
font-size: 14px !important;
cursor: pointer !important;
transition: background .13s !important;
}
ul.options li:hover, .options li:hover {
background: rgba(127,90,240,0.22) !important;
color: #ede8ff !important;
}
ul.options li.selected, .options li.selected {
background: rgba(80,30,160,0.40) !important;
color: #ede8ff !important;
font-weight: 700 !important;
}
/* ============================================================
BUTTONS
============================================================ */
/* Primary */
button.primary,
button[variant="primary"],
.gr-button.primary {
background: linear-gradient(135deg, #6d28d9 0%, #4338ca 60%, #1d6a50 100%) !important;
color: #fff !important;
font-weight: 700 !important;
font-size: 14.5px !important;
letter-spacing: 0.3px !important;
border: none !important;
border-radius: 11px !important;
padding: 12px 26px !important;
transition: transform .22s ease, box-shadow .22s ease, filter .22s ease !important;
box-shadow: 0 4px 18px rgba(109,40,217,0.38),
0 1px 0 rgba(255,255,255,0.10) inset !important;
cursor: pointer !important;
}
button.primary:hover,
button[variant="primary"]:hover {
transform: translateY(-2px) !important;
box-shadow: 0 8px 28px rgba(109,40,217,0.55),
0 1px 0 rgba(255,255,255,0.14) inset !important;
filter: brightness(1.07) !important;
}
button.primary:active,
button[variant="primary"]:active {
transform: translateY(0) scale(0.98) !important;
box-shadow: 0 2px 8px rgba(109,40,217,0.30) !important;
}
/* Secondary */
button.secondary,
button[variant="secondary"],
.gr-button.secondary {
background: rgba(109,40,217,0.08) !important;
color: #a880ff !important;
border: 1.5px solid rgba(109,40,217,0.30) !important;
border-radius: 10px !important;
font-weight: 600 !important;
font-size: 13.5px !important;
transition: all .2s ease !important;
cursor: pointer !important;
}
button.secondary:hover,
button[variant="secondary"]:hover {
background: rgba(109,40,217,0.20) !important;
border-color: rgba(109,40,217,0.60) !important;
color: #c8a8ff !important;
transform: translateY(-1px) !important;
box-shadow: 0 4px 16px rgba(109,40,217,0.24) !important;
}
/* ============================================================
MARKDOWN OUTPUT
============================================================ */
.gr-markdown, .prose {
color: #c4c4dc !important;
line-height: 1.80 !important;
}
.gr-markdown h1 {
font-size: 1.75rem !important;
font-weight: 800 !important;
background: linear-gradient(90deg, #a080f8, #34d399) !important;
-webkit-background-clip: text !important;
-webkit-text-fill-color: transparent !important;
margin-bottom: 12px !important;
}
.gr-markdown h2 {
color: #9f7aea !important;
font-size: 1.10rem !important;
font-weight: 700 !important;
border-left: 3px solid #7f5af0 !important;
padding-left: 10px !important;
margin-top: 22px !important;
margin-bottom: 8px !important;
}
.gr-markdown h3 {
color: #34d399 !important;
font-size: 1rem !important;
font-weight: 600 !important;
margin-top: 14px !important;
}
.gr-markdown strong { color: #e2d8ff !important; }
.gr-markdown code {
background: rgba(127,90,240,0.16) !important;
color: #e8c4ff !important;
border-radius: 4px !important;
padding: 2px 7px !important;
font-family: 'Fira Code', 'Cascadia Code', 'Consolas', monospace !important;
font-size: 0.86em !important;
}
.gr-markdown pre {
background: rgba(4,4,16,0.88) !important;
border: 1px solid rgba(127,90,240,0.22) !important;
border-radius: 11px !important;
padding: 16px !important;
}
.gr-markdown pre code {
background: transparent !important;
border: none !important;
padding: 0 !important;
}
.gr-markdown table { border-collapse: collapse !important; width: 100% !important; margin: 12px 0 !important; }
.gr-markdown th {
background: rgba(127,90,240,0.22) !important;
color: #ddd0ff !important;
padding: 9px 12px !important;
text-align: left !important;
font-weight: 700 !important;
}
.gr-markdown td {
border-bottom: 1px solid rgba(255,255,255,0.055) !important;
padding: 8px 12px !important;
color: #bebede !important;
}
.gr-markdown tr:hover td { background: rgba(127,90,240,0.07) !important; }
.gr-markdown ul, .gr-markdown ol { padding-left: 20px !important; }
.gr-markdown li { margin: 5px 0 !important; }
/* ============================================================
FILE UPLOAD
============================================================ */
.file-preview, [class*="upload"] {
border: 2px dashed rgba(127,90,240,0.32) !important;
border-radius: 12px !important;
background: rgba(127,90,240,0.04) !important;
color: #7070a8 !important;
transition: border-color .2s, background .2s !important;
}
.file-preview:hover {
border-color: rgba(127,90,240,0.60) !important;
background: rgba(127,90,240,0.08) !important;
}
/* ============================================================
DATAFRAME / TABLE
============================================================ */
.gr-dataframe table th {
background: rgba(127,90,240,0.20) !important;
color: #d0c8ff !important;
font-weight: 700 !important;
}
.gr-dataframe table td {
color: #b8b8d8 !important;
border-bottom: 1px solid rgba(255,255,255,0.05) !important;
}
.gr-dataframe table tr:hover td {
background: rgba(127,90,240,0.08) !important;
}
/* ============================================================
SCROLLBAR
============================================================ */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(127,90,240,0.45); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(127,90,240,0.70); }
/* ============================================================
MISC
============================================================ */
hr {
border: none !important;
border-top: 1px solid rgba(255,255,255,0.055) !important;
margin: 16px 0 !important;
}
"""
# ── Shared HTML fragments ─────────────────────────────────────────────────────
_HEADER = """
<div style="
text-align: center;
padding: 32px 24px 20px;
background: linear-gradient(180deg, rgba(100,60,220,0.06) 0%, transparent 100%);
border-bottom: 1px solid rgba(100,60,220,0.12);
margin-bottom: 8px;
">
<div style="
font-size: 2.4rem;
font-weight: 900;
background: linear-gradient(90deg, #a080f8, #34d399);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -1px;
line-height: 1.1;
margin-bottom: 10px;
">AI Study Helper</div>
<div style="
color: #5a5a8a;
font-size: 0.92rem;
font-weight: 500;
letter-spacing: 0.5px;
margin-bottom: 18px;
">B.Tech CSE &middot; Mistral 7B &amp; Llama 3.1 8B via Hugging Face</div>
<div style="display: flex; gap: 8px; justify-content: center; flex-wrap: wrap;">
<span style="background:rgba(127,90,240,0.12);color:#a080f8;padding:4px 14px;border-radius:20px;font-size:11px;border:1px solid rgba(127,90,240,0.22);font-weight:600;letter-spacing:0.4px;">LEARN</span>
<span style="background:rgba(44,182,125,0.09);color:#2cb67d;padding:4px 14px;border-radius:20px;font-size:11px;border:1px solid rgba(44,182,125,0.20);font-weight:600;letter-spacing:0.4px;">REVISION</span>
<span style="background:rgba(245,168,0,0.09);color:#f5a800;padding:4px 14px;border-radius:20px;font-size:11px;border:1px solid rgba(245,168,0,0.18);font-weight:600;letter-spacing:0.4px;">QUIZ</span>
<span style="background:rgba(240,96,96,0.08);color:#f06060;padding:4px 14px;border-radius:20px;font-size:11px;border:1px solid rgba(240,96,96,0.18);font-weight:600;letter-spacing:0.4px;">INTERVIEW</span>
<span style="background:rgba(0,200,248,0.07);color:#00c8f8;padding:4px 14px;border-radius:20px;font-size:11px;border:1px solid rgba(0,200,248,0.16);font-weight:600;letter-spacing:0.4px;">UPLOAD</span>
<span style="background:rgba(160,168,248,0.07);color:#a0a8f8;padding:4px 14px;border-radius:20px;font-size:11px;border:1px solid rgba(160,168,248,0.16);font-weight:600;letter-spacing:0.4px;">HISTORY</span>
</div>
</div>
"""
_FOOTER = """
<div style="
text-align: center;
padding: 14px 0 10px;
color: rgba(255,255,255,0.16);
font-size: 11px;
border-top: 1px solid rgba(255,255,255,0.05);
margin-top: 16px;
letter-spacing: 0.3px;
">
AI Study Helper v5.1 &nbsp;&middot;&nbsp; Mistral 7B / Llama 3.1 via Hugging Face
&nbsp;&middot;&nbsp; localhost:7860
</div>
"""
# ══════════════════════════════════════════════════════════════════════════════
# Gradio UI
# ══════════════════════════════════════════════════════════════════════════════
def _subject_row(prefix: str) -> tuple:
"""Shared subject + topic dropdown row, returns (cat_dd, top_dd)."""
with gr.Row():
cat = gr.Dropdown(
choices=list(TOPICS.keys()), value=_FIRST_CAT,
label="Subject", interactive=True, allow_custom_value=False,
)
top = gr.Dropdown(
choices=_FIRST_TOPICS, value=_FIRST_TOPICS[0],
label="Topic", interactive=True, allow_custom_value=False,
)
cat.change(fn=update_topics, inputs=cat, outputs=top)
return cat, top
def _pdf_row(out_md, pdf_fn, label="Download as PDF"):
"""Add a PDF download button + clear button below any output."""
with gr.Row():
pdf_btn = gr.Button(f"{label}", variant="secondary")
clr_btn = gr.Button("Clear", variant="secondary")
pdf_out = gr.File(label="Download")
pdf_btn.click(fn=pdf_fn, inputs=out_md, outputs=pdf_out)
clr_btn.click(fn=lambda: ("", None), inputs=[], outputs=[out_md, pdf_out])
return pdf_out
_JS = """
(function() {
"use strict";
function syncRadioClasses() {
// For each radio group, mark only the checked label
var groups = {};
document.querySelectorAll('.gr-radio input[type="radio"]').forEach(function(inp) {
var name = inp.name || inp.closest('fieldset')?.id || 'default';
if (!groups[name]) groups[name] = [];
groups[name].push(inp);
});
Object.values(groups).forEach(function(inputs) {
inputs.forEach(function(inp) {
var label = inp.closest('label');
if (!label) return;
if (inp.checked) {
label.classList.add('radio-selected');
} else {
label.classList.remove('radio-selected');
}
});
});
}
// Re-sync on any click inside .gr-radio containers
document.addEventListener('click', function(e) {
var label = e.target.closest('.gr-radio label');
if (label) {
// Give Gradio a moment to update its state
setTimeout(syncRadioClasses, 30);
setTimeout(syncRadioClasses, 150);
}
});
// Re-sync whenever Gradio re-renders components
var mo = new MutationObserver(function(mutations) {
var relevant = mutations.some(function(m) {
return m.target.closest && m.target.closest('.gr-radio');
});
if (relevant) setTimeout(syncRadioClasses, 20);
});
mo.observe(document.body, { childList: true, subtree: true, attributes: true,
attributeFilter: ['checked'] });
// Initial sync passes
setTimeout(syncRadioClasses, 400);
setTimeout(syncRadioClasses, 1000);
setTimeout(syncRadioClasses, 2500);
})();
"""
with gr.Blocks(title="AI Study Helper") as demo:
gr.HTML(_HEADER)
with gr.Tabs():
# â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â•
# TAB 1 — Learn
# â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â•
with gr.Tab("Learn"):
gr.Markdown("Generate structured AI study notes - select subject, topic, level and mode.")
l_cat, l_top = _subject_row("l")
with gr.Row():
l_lvl = gr.Dropdown(
choices=["Easy", "Intermediate", "Advanced"],
value="Easy", label="Difficulty",
interactive=True, allow_custom_value=False,
)
l_mode = gr.Dropdown(
list(_VALID_MODES), value="Learn Concept",
label="Study Mode", interactive=True, allow_custom_value=False,
)
l_btn = gr.Button("Generate Notes", variant="primary", size="lg")
l_out = gr.Markdown("*Your AI notes will appear here.*")
l_btn.click(fn=generate_notes, inputs=[l_cat, l_top, l_lvl, l_mode], outputs=l_out)
_pdf_row(l_out, notes_pdf, "Download Notes PDF")
# â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â•
# TAB 2 — Revision
# â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â•
with gr.Tab("Revision"):
gr.Markdown("Type any topic - AI creates crisp last-minute revision notes.")
r_top = gr.Textbox(
label="Topic to Revise",
placeholder="e.g. TCP/IP Handshake | Deadlocks | Merge Sort",
lines=2,
)
r_btn = gr.Button("Generate Revision Notes", variant="primary", size="lg")
r_out = gr.Markdown("*Revision notes will appear here.*")
r_btn.click(fn=generate_revision, inputs=r_top, outputs=r_out)
_pdf_row(r_out, revision_pdf, "Download Revision PDF")
# â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â•
# TAB 3 — Quiz (interactive MCQ)
# ══════════════════════════════════════════════════════════════════
with gr.Tab("Quiz"):
gr.Markdown(
"AI generates **5 MCQ questions** with 4 options each. "
"Select your answers then click **Submit Quiz** for instant feedback."
)
quiz_state = gr.State([])
qz_cat, qz_top = _subject_row("qz")
qz_lvl = gr.Dropdown(
choices=["Easy", "Intermediate", "Advanced"],
value="Intermediate", label="Difficulty",
interactive=True, allow_custom_value=False,
)
qz_gen = gr.Button("Generate Quiz", variant="primary", size="lg")
qz_status = gr.Markdown()
# Pre-create 5 question slots (hidden until quiz is generated)
# Dropdown widgets for answer selection (consistent with Subject/Topic/Level UI)
qz_groups, qz_qmds, qz_radios = [], [], []
for i in range(5):
with gr.Group(visible=False) as g:
qmd = gr.Markdown(f"Q{i + 1}.")
qrad = gr.Dropdown(
choices=[], label="Select your answer:",
interactive=True, value=None,
allow_custom_value=False,
)
qz_groups.append(g)
qz_qmds.append(qmd)
qz_radios.append(qrad)
qz_submit = gr.Button("Submit Quiz", variant="primary", visible=False)
with gr.Group(visible=False) as qz_res_grp:
qz_res_md = gr.Markdown()
qz_score = gr.Markdown()
# Wire generate — 19 outputs total
_gen_outs = [quiz_state, qz_status]
for i in range(5):
_gen_outs += [qz_groups[i], qz_qmds[i], qz_radios[i]]
_gen_outs += [qz_submit, qz_res_grp]
qz_gen.click(fn=generate_quiz_fn,
inputs=[qz_cat, qz_top, qz_lvl],
outputs=_gen_outs)
# Wire submit — 3 outputs
qz_submit.click(
fn=check_quiz_fn,
inputs=[quiz_state] + qz_radios,
outputs=[qz_res_grp, qz_res_md, qz_score],
)
# PDF download (downloads the results markdown)
with gr.Row():
qz_pdf_btn = gr.Button("Download Quiz PDF", variant="secondary")
qz_pdf = gr.File(label="Download")
qz_pdf_btn.click(fn=quiz_pdf, inputs=qz_res_md, outputs=qz_pdf)
# â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â•
# TAB 4 — Interview Prep
# â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â•
with gr.Tab("Interview Prep"):
gr.Markdown("Get a complete placement interview pack - technical Q&A, coding, and HR tips.")
i_cat, i_top = _subject_row("iv")
i_btn = gr.Button("Generate Interview Pack", variant="primary", size="lg")
i_out = gr.Markdown("*Interview Q&A will appear here.*")
i_btn.click(fn=generate_interview, inputs=[i_cat, i_top], outputs=i_out)
_pdf_row(i_out, interview_pdf, "Download Interview PDF")
# â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â•
# TAB 5 — Upload Notes
# â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â• â•
with gr.Tab("Upload Notes"):
gr.Markdown("Upload your PDF or TXT notes - AI summarises, extracts key points and generates exam questions.")
with gr.Row(equal_height=False):
with gr.Column(scale=1):
gr.Markdown("#### Upload Panel")
u_file = gr.File(label="Drop PDF or TXT here", file_types=[".pdf", ".txt"])
u_btn = gr.Button("Analyse Notes", variant="primary", size="lg")
with gr.Column(scale=2):
gr.Markdown("#### AI Summary")
u_out = gr.Markdown("*Upload a file and click Analyse Notes.*")
u_btn.click(fn=analyze_upload, inputs=u_file, outputs=u_out)
_pdf_row(u_out, upload_pdf, "Download Summary PDF")
# ══════════════════════════════════════════════════════════════════
# TAB 6 — History
# ══════════════════════════════════════════════════════════════════
with gr.Tab("History"):
gr.Markdown(
"Persistent session log ” stored in `study_history.json`. "
"Last 50 entries shown."
)
with gr.Row():
h_refresh = gr.Button("Refresh", variant="primary")
h_clear = gr.Button("Clear History", variant="secondary")
h_table = gr.DataFrame(
headers=["#", "Timestamp", "Tab", "Topic / File", "Mode . Level"],
datatype=["number", "str", "str", "str", "str"],
interactive=False,
label="Generation History",
)
h_status = gr.Markdown()
h_refresh.click(fn=get_history_data, inputs=[], outputs=h_table)
h_clear.click(fn=clear_history_fn, inputs=[], outputs=[h_table, h_status])
gr.HTML(_FOOTER)
# ── Launch ────────────────────────────────────────────────────────────────────
# Binds to 127.0.0.1 only — do NOT change to 0.0.0.0 without adding auth.
demo.launch(server_name="0.0.0.0", server_port=7860)