""" 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 = """
AI Study Helper
B.Tech CSE · Mistral 7B & Llama 3.1 8B via Hugging Face
LEARN REVISION QUIZ INTERVIEW UPLOAD HISTORY
""" _FOOTER = """
AI Study Helper v5.1  ·  Mistral 7B / Llama 3.1 via Hugging Face  ·  localhost:7860
""" # ══════════════════════════════════════════════════════════════════════════════ # 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)