Spaces:
Sleeping
Sleeping
| """ | |
| 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 · Mistral 7B & 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 · Mistral 7B / Llama 3.1 via Hugging Face | |
| · 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) | |