| """ |
| PaperProf — Main Gradio entry point. |
| """ |
|
|
| import warnings |
| warnings.filterwarnings("ignore") |
|
|
| try: |
| import os |
| import random |
| import pathlib |
| import threading |
| import gradio as gr |
| import spaces |
| from core.parser import extract_text |
| from core.chunker import chunk_text |
| from core.questioner import generate_question, generate_mcq |
| from core.evaluator import evaluate_answer |
| from core.image_gen import generate_concept_image |
| from model.llm import get_llm |
| print("✅ All imports successful") |
| except Exception as e: |
| import traceback |
| print(f"❌ Import error: {e}") |
| traceback.print_exc() |
| raise |
|
|
| RUNTIME = os.environ.get("PAPERPROF_RUNTIME", "transformers").lower() |
|
|
|
|
| def gpu_if_needed(duration): |
| """@spaces.GPU only for the transformers runtime — llama.cpp runs on CPU |
| and must not be capped by (or waste) a ZeroGPU window.""" |
| def wrap(fn): |
| return spaces.GPU(duration=duration)(fn) if RUNTIME != "llamacpp" else fn |
| return wrap |
|
|
|
|
| if RUNTIME == "llamacpp": |
| |
| |
| threading.Thread(target=get_llm, daemon=True).start() |
|
|
| CSS = """ |
| /* ── Base ──────────────────────────────────────────────────────────────────── */ |
| footer { display: none !important; } |
| /* Only hide Gradio's own header chrome — NOT our custom <header class="hero"> */ |
| .gradio-container > header, |
| .app > header { display: none !important; } |
| * { box-sizing: border-box; } |
| body { background: #0A0F1E !important; margin: 0 !important; } |
| |
| /* ── Gradio shell ───────────────────────────────────────────────────────────── */ |
| .gradio-container { |
| padding: 0 !important; |
| margin: 0 !important; |
| max-width: 100% !important; |
| background: #0A0F1E !important; |
| min-height: 0 !important; |
| } |
| .main { padding: 0 !important; } |
| .gap { gap: 0 !important; } |
| .contain { padding: 0 !important; } |
| #component-0 { padding: 0 !important; } |
| |
| /* ── Upload row: centred column, card look ──────────────────────────────────── */ |
| #upload-row { |
| max-width: 760px !important; |
| margin: 0 auto !important; |
| padding: 20px !important; |
| flex-direction: column !important; |
| gap: 12px !important; |
| background: rgba(10,15,30,0.65) !important; |
| border: 1px solid rgba(255,255,255,0.08) !important; |
| border-radius: 18px !important; |
| backdrop-filter: blur(24px) !important; |
| -webkit-backdrop-filter: blur(24px) !important; |
| box-shadow: 0 8px 32px rgba(0,0,0,0.45) !important; |
| } |
| |
| /* ── Rows are moved off-screen but NOT display:none so Gradio attaches event |
| handlers; height:0 + overflow:visible let their textboxes bleed into the |
| page as a dark full-width bar, so park them off-viewport instead. ── */ |
| #hidden-row-status, |
| #hidden-row-question { |
| position: fixed !important; |
| top: -9999px !important; |
| left: -9999px !important; |
| width: 1px !important; |
| height: 1px !important; |
| min-height: 0 !important; |
| overflow: hidden !important; |
| opacity: 0 !important; |
| pointer-events: none !important; |
| margin: 0 !important; |
| padding: 0 !important; |
| border: none !important; |
| gap: 0 !important; |
| } |
| |
| /* ── Force hero text to be visible (defeat Gradio color resets) ─────────────── */ |
| .hero-title { |
| color: #A78BFA !important; |
| background: linear-gradient(135deg, #A78BFA 0%, #67E8F9 100%) !important; |
| -webkit-background-clip: text !important; |
| -webkit-text-fill-color: transparent !important; |
| background-clip: text !important; |
| } |
| .hero-sub { color: #94A3B8 !important; } |
| .hero-icon { color: initial !important; font-size: 3.2rem !important; display: block !important; } |
| |
| /* ── Upload zone card ───────────────────────────────────────────────────────── */ |
| /* Gradio's own block/file wrappers paint a beige fill on dark theme — make |
| everything inside the upload row transparent so the glass card shows. */ |
| #upload-row .block, |
| #upload-row .file, |
| #upload-row .gradio-file, |
| #upload-row .wrap, |
| #upload-row [data-testid="block"] { |
| background: transparent !important; |
| border-color: transparent !important; |
| } |
| [data-testid="upload-zone"], |
| .file-drop-zone, |
| .upload-container { |
| background: rgba(255,255,255,0.03) !important; |
| border: 2px dashed rgba(124,58,237,0.4) !important; |
| border-radius: 14px !important; |
| min-height: 130px !important; |
| color: rgba(255,255,255,0.65) !important; |
| text-align: center !important; |
| transition: border-color .2s, box-shadow .2s, background .2s !important; |
| backdrop-filter: blur(12px) !important; |
| -webkit-backdrop-filter: blur(12px) !important; |
| } |
| [data-testid="upload-zone"]:hover, |
| .file-drop-zone:hover { |
| border-color: #06B6D4 !important; |
| background: rgba(6,182,212,0.04) !important; |
| box-shadow: 0 0 28px rgba(6,182,212,0.15) !important; |
| } |
| [data-testid="upload-zone"] *, |
| .file-drop-zone * { color: rgba(255,255,255,0.7) !important; } |
| |
| /* ── File preview after selection ──────────────────────────────────────────── */ |
| .file-preview-holder, |
| .file-name-wrapper, |
| .file-preview { |
| background: rgba(124,58,237,0.07) !important; |
| border: 1px solid rgba(124,58,237,0.2) !important; |
| border-radius: 10px !important; |
| color: #A78BFA !important; |
| } |
| .file-preview * { color: #A78BFA !important; } |
| |
| /* ── Hide all Gradio component labels ──────────────────────────────────────── */ |
| .block > label, |
| .block > .form > label, |
| .label-wrap, |
| .block label span { display: none !important; } |
| |
| /* ── Load PDF button — full-width gradient pill ─────────────────────────────── */ |
| #hidden-load-btn { width: 100% !important; margin-top: 12px !important; } |
| #hidden-load-btn button { |
| width: 100% !important; |
| background: linear-gradient(135deg, #7C3AED, #06B6D4) !important; |
| border-radius: 50px !important; |
| color: #fff !important; |
| font-weight: 700 !important; |
| font-size: 1.1rem !important; |
| letter-spacing: 0.5px !important; |
| border: none !important; |
| padding: 16px !important; |
| box-shadow: 0 4px 20px rgba(124,58,237,0.35) !important; |
| transition: box-shadow .2s, transform .2s !important; |
| } |
| #hidden-load-btn button:hover { |
| box-shadow: 0 6px 28px rgba(124,58,237,0.55) !important; |
| transform: translateY(-1px) !important; |
| } |
| |
| /* ── Post-load: upload area recedes ────────────────────────────────────────── */ |
| .upload-receded { |
| opacity: 0.45 !important; |
| transform: scale(0.98) !important; |
| transition: opacity .4s ease, transform .4s ease !important; |
| pointer-events: none !important; |
| } |
| |
| /* ── Standalone hidden components (no row wrapper) ──────────────────────────── */ |
| #hidden-feedback-output, |
| #hidden-answer-input, |
| #hidden-submit-btn, |
| #hidden-question-btn, |
| #hidden-question-trigger, |
| #hidden-answer-trigger, |
| #hidden-chunk-output, |
| #hidden-mcq-trigger, |
| #hidden-mcq-output, |
| #hidden-language-box, |
| #hidden-session-topics, |
| #hidden-generate-session-image-btn { |
| position: fixed !important; |
| top: -9999px !important; |
| left: -9999px !important; |
| width: 1px !important; |
| height: 1px !important; |
| overflow: hidden !important; |
| opacity: 0 !important; |
| pointer-events: none !important; |
| } |
| |
| /* ── Hidden concept image component ─────────────────────────────────────────── */ |
| #hidden-concept-image { |
| position: fixed !important; |
| top: -9999px !important; |
| opacity: 0 !important; |
| } |
| |
| /* ── Empty .form wrappers ─────────────────────────────────────────────────────── |
| Gradio groups consecutive textboxes into bordered .form containers; ours are |
| all parked off-screen, leaving 3 empty bars at the page bottom — hide them. */ |
| .form:has(> #hidden-answer-input), |
| .form:has(> #hidden-feedback-output), |
| .form:has(> #hidden-question-trigger), |
| .form:has(> #hidden-session-topics) { |
| position: fixed !important; |
| top: -9999px !important; |
| left: -9999px !important; |
| width: 1px !important; |
| height: 1px !important; |
| border: none !important; |
| } |
| /* fallback for browsers without :has — at least make the bars invisible */ |
| .gradio-container .form { |
| background: transparent !important; |
| border-color: transparent !important; |
| box-shadow: none !important; |
| } |
| |
| /* ── Load PDF button — defeat Gradio's orange primary override ──────────────── */ |
| .gr-button-primary, |
| button.primary, |
| #hidden-load-btn button, |
| #hidden-load-btn .gr-button { |
| background: linear-gradient(135deg, #7C3AED, #06B6D4) !important; |
| background-color: #7C3AED !important; |
| } |
| """ |
|
|
| import pathlib |
| CUSTOM_HTML = pathlib.Path("ui/index.html").read_text() |
|
|
| |
| |
| |
|
|
| UI = { |
| "English": { |
| "load_btn": "Load PDF", |
| "new_q_btn": "New Question", |
| "submit_btn": "Submit Answer", |
| "pdf_label": "📎 Course PDF", |
| "status_label": "Status", |
| "question_label": "❓ Question", |
| "answer_label": "✏️ Your Answer", |
| "answer_ph": "Write your answer here…", |
| "feedback_label": "💬 Feedback", |
| "no_file": "No file selected.", |
| "no_chunks": "PDF loaded but no text found (scanned or too short?).", |
| "loaded": lambda n: f"📚 {n} sections extracted — ready to generate questions!", |
| "no_pdf": "⚠️ Please upload a PDF first.", |
| "no_question": "⚠️ Generate a question first.", |
| "no_answer": "⚠️ Please write an answer before submitting.", |
| }, |
| "Français": { |
| "load_btn": "Charger le PDF", |
| "new_q_btn": "Nouvelle question", |
| "submit_btn": "Valider la réponse", |
| "pdf_label": "📎 Cours PDF", |
| "status_label": "Statut", |
| "question_label": "❓ Question", |
| "answer_label": "✏️ Ta réponse", |
| "answer_ph": "Écris ta réponse ici…", |
| "feedback_label": "💬 Feedback", |
| "no_file": "Aucun fichier sélectionné.", |
| "no_chunks": "PDF chargé mais aucun texte trouvé (scanné ou trop court ?).", |
| "loaded": lambda n: f"📚 {n} sections extraites — prêt à générer des questions !", |
| "no_pdf": "⚠️ Charge d'abord un PDF.", |
| "no_question": "⚠️ Génère d'abord une question.", |
| "no_answer": "⚠️ Écris une réponse avant de valider.", |
| }, |
| } |
|
|
| |
| |
| |
|
|
| def _parse_verdict(feedback: str) -> bool: |
| for line in feedback.splitlines(): |
| low = line.lower() |
| if "verdict" in low: |
| return "correct" in low and "partially" not in low and "incorrect" not in low |
| return False |
|
|
|
|
| def _score_label(correct: int, total: int) -> str: |
| return f"✅ {correct} / {total}" if total > 0 else "—" |
|
|
|
|
| def update_ui_language(language): |
| s = UI[language] |
| return ( |
| gr.update(value=s["load_btn"]), |
| gr.update(value=s["new_q_btn"]), |
| gr.update(value=s["submit_btn"]), |
| gr.update(label=s["pdf_label"]), |
| gr.update(label=s["status_label"]), |
| gr.update(label=s["question_label"]), |
| gr.update(label=s["answer_label"], placeholder=s["answer_ph"]), |
| gr.update(label=s["feedback_label"]), |
| ) |
|
|
|
|
| def load_pdf(pdf_file, language): |
| print(f"load_pdf called with: {pdf_file}") |
| s = UI[language] |
| if pdf_file is None: |
| return [], s["no_file"], 0, 0 |
| try: |
| text = extract_text(pdf_file.name) |
| chunks = chunk_text(text) |
| print(f"Extracted chunks: {len(chunks)}") |
| except ValueError as e: |
| print(f"load_pdf ValueError: {e}") |
| return [], f"Erreur : {e}", 0, 0 |
| except Exception as e: |
| print(f"load_pdf Exception: {e}") |
| return [], f"Erreur inattendue : {e}", 0, 0 |
| if not chunks: |
| return [], s["no_chunks"], 0, 0 |
| return chunks, s["loaded"](len(chunks)), 0, 0 |
|
|
|
|
| @gpu_if_needed(duration=180) |
| def new_question(chunks, language): |
| print(f"[new_question] called — {len(chunks)} chunks, lang={language}") |
| s = UI[language] |
| if not chunks: |
| print("[new_question] no chunks, returning early") |
| return s["no_pdf"], "", s["no_pdf"], "" |
| try: |
| chunk = random.choice(chunks) |
| print(f"[new_question] chunk selected ({len(chunk)} chars), calling LLM…") |
| question = generate_question(chunk, language=language) |
| print(f"[new_question] question generated: {question[:80]!r}") |
| sentences = [s.strip() for s in chunk.replace('\n', ' ').split('.') if s.strip()] |
| preview = '. '.join(sentences[:3]) + ('.' if sentences else '') |
| if len(preview) > 400: |
| preview = preview[:400].rsplit(' ', 1)[0] + '…' |
| return question, chunk, "", preview |
| except Exception as e: |
| import traceback; traceback.print_exc() |
| err = f"Erreur : {e}" |
| return err, "", err, "" |
|
|
|
|
| @gpu_if_needed(duration=180) |
| def new_mcq(chunks, language): |
| import json as _json |
| print(f"[new_mcq] called — {len(chunks)} chunks, lang={language}") |
| s = UI[language] |
| if not chunks: |
| return s["no_pdf"], "", _json.dumps({}), "" |
| try: |
| chunk = random.choice(chunks) |
| print(f"[new_mcq] chunk selected ({len(chunk)} chars), calling LLM…") |
| mcq = generate_mcq(chunk, language=language) |
| print(f"[new_mcq] MCQ generated: {mcq.get('question','?')[:60]!r}") |
| sentences = [x.strip() for x in chunk.replace('\n', ' ').split('.') if x.strip()] |
| preview = '. '.join(sentences[:3]) + ('.' if sentences else '') |
| if len(preview) > 400: |
| preview = preview[:400].rsplit(' ', 1)[0] + '…' |
| return mcq.get("question", ""), chunk, _json.dumps(mcq), preview |
| except Exception as e: |
| import traceback; traceback.print_exc() |
| err = f"Erreur : {e}" |
| return err, "", _json.dumps({"error": err}), "" |
|
|
|
|
| @gpu_if_needed(duration=120) |
| def submit_answer(question, chunk, student_answer, correct, total, language): |
| s = UI[language] |
| if not question: |
| return s["no_question"], correct, total, _score_label(correct, total) |
| if not student_answer.strip(): |
| return s["no_answer"], correct, total, _score_label(correct, total) |
| try: |
| feedback = evaluate_answer(question, chunk, student_answer, language=language) |
| total += 1 |
| if _parse_verdict(feedback): |
| correct += 1 |
| return feedback, correct, total, _score_label(correct, total) |
| except Exception as e: |
| return f"Erreur : {e}", correct, total, _score_label(correct, total) |
|
|
|
|
| @spaces.GPU(duration=120) |
| def generate_image_fn(topics): |
| if not topics or not topics.strip(): |
| return None |
| try: |
| prompt = f"Visual summary of a study session covering: {topics.strip()}" |
| print(f"[generate_image_fn] generating session illustration for: {prompt[:120]!r}") |
| return generate_concept_image(prompt) |
| except Exception as e: |
| import traceback; traceback.print_exc() |
| print(f"[generate_image_fn] failed: {e}") |
| return None |
|
|
|
|
| |
| |
| |
|
|
| BRIDGE_JS = """() => { |
| // ── State ────────────────────────────────────────────────────────────────── |
| const S = { |
| correct: 0, total: 0, history: [], |
| currentQuestion: '', |
| quizRevealed: false, |
| lastStatus: '', |
| waitingQ: false, prevQuestion: '', |
| waitingFB: false, prevFeedback: '', |
| mode: 'open', |
| mcqData: null, |
| waitingMCQ: false, prevMCQ: '', |
| imgPoll: null, imgRequested: false, |
| }; |
| const $ = id => document.getElementById(id); |
| |
| // ── Status bar ───────────────────────────────────────────────────────────── |
| function ensureStatusBar() { |
| if ($('gradio-status-bar')) return; |
| const anchor = $('quiz-card') || $('app'); |
| if (!anchor) return; |
| const bar = document.createElement('div'); |
| bar.id = 'gradio-status-bar'; |
| bar.style.cssText = 'display:none;max-width:760px;margin:8px auto 0;padding:12px 20px;border-radius:10px;font-size:.9rem;font-weight:500;font-family:Inter,system-ui,sans-serif;transition:all .3s ease;'; |
| anchor.parentNode.insertBefore(bar, anchor); |
| } |
| |
| function applyStatus(text) { |
| if (text === S.lastStatus) return; |
| S.lastStatus = text; |
| ensureStatusBar(); |
| const bar = $('gradio-status-bar'); |
| if (!bar) return; |
| bar.textContent = text; |
| bar.style.display = 'block'; |
| const isErr = text.includes('\\u26a0') || text.toLowerCase().includes('erreur'); |
| const isOK = text.includes('sections') || text.includes('extracted') || text.includes('extraites'); |
| if (isErr) { |
| bar.style.cssText += ';background:rgba(239,68,68,.12);border:1px solid rgba(239,68,68,.3);color:#FCA5A5'; |
| } else if (isOK) { |
| bar.style.cssText += ';background:rgba(16,185,129,.12);border:1px solid rgba(16,185,129,.3);color:#6EE7B7'; |
| if (!S.quizRevealed) { S.quizRevealed = true; revealQuiz(); } |
| } else { |
| bar.style.cssText += ';background:rgba(6,182,212,.12);border:1px solid rgba(6,182,212,.3);color:#67E8F9'; |
| } |
| } |
| |
| function revealQuiz() { |
| const qc = $('quiz-card'); |
| if (qc) { qc.classList.remove('hidden'); setTimeout(() => qc.scrollIntoView({behavior:'smooth',block:'start'}), 400); } |
| const ur = document.querySelector('#upload-row'); |
| if (ur) { ur.style.cssText += ';opacity:0;height:0;overflow:hidden;margin:0;padding:0;pointer-events:none;transition:all .4s ease'; } |
| } |
| |
| // ── Helpers ──────────────────────────────────────────────────────────────── |
| function setGradioTA(sel, val) { |
| const el = document.querySelector(sel); |
| if (!el) return false; |
| Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,'value').set.call(el, val); |
| el.dispatchEvent(new Event('input', {bubbles:true})); |
| el.dispatchEvent(new Event('change', {bubbles:true})); |
| return true; |
| } |
| function clickGradioBtn(sel) { |
| const el = document.querySelector(sel); |
| if (!el) return false; |
| const btn = el.tagName === 'BUTTON' ? el : el.querySelector('button'); |
| if (btn) { btn.click(); return true; } return false; |
| } |
| |
| // ── Hero: professor eye tracking ─────────────────────────────────────────── |
| // (Lives here, not in index.html: <script> tags injected via gr.HTML's |
| // innerHTML are never executed by browsers.) |
| function initHeroProfessor() { |
| const prof = $('professor'); |
| if (!prof || prof._pp) return; |
| prof._pp = true; |
| const pupils = prof.querySelectorAll('.prof-pupil'); |
| let tx = 0, ty = 0, cx = 0, cy = 0; |
| document.addEventListener('mousemove', e => { |
| const r = prof.getBoundingClientRect(); |
| // eyes sit in the head, around the top third of the character |
| const ox = r.left + r.width / 2, oy = r.top + r.height * 0.32; |
| const dx = e.clientX - ox, dy = e.clientY - oy; |
| const d = Math.min(4, Math.hypot(dx, dy) / 40); // max 4px displacement |
| const a = Math.atan2(dy, dx); |
| tx = Math.cos(a) * d; ty = Math.sin(a) * d; |
| }); |
| (function track() { |
| cx += (tx - cx) * 0.18; cy += (ty - cy) * 0.18; |
| const t = 'translate(' + cx.toFixed(2) + 'px,' + cy.toFixed(2) + 'px)'; |
| pupils.forEach(p => { p.style.transform = t; }); |
| requestAnimationFrame(track); |
| })(); |
| } |
| |
| // ── Library wings: span the whole page, repeat content to the bottom ────── |
| // position:fixed breaks under Gradio's ancestor transforms, so the wings are |
| // re-parented onto <body>, stretched to the page height, and their .lib-unit |
| // content is cloned as many times as needed to fill it. |
| function syncLibraryWings() { |
| const wings = document.querySelectorAll('.library'); |
| if (!wings.length) return; |
| wings.forEach(w => { if (w.parentElement !== document.body) document.body.appendChild(w); }); |
| const gc = document.querySelector('.gradio-container'); |
| const H = Math.max(gc ? gc.scrollHeight : 0, window.innerHeight); |
| wings.forEach(w => { |
| if (parseInt(w.style.height, 10) !== H) w.style.height = H + 'px'; |
| const first = w.querySelector('.lib-unit'); |
| if (!first) return; |
| const per = first.offsetHeight || 950; |
| const need = Math.max(1, Math.ceil(H / per)); |
| let units = w.querySelectorAll('.lib-unit'); |
| for (let i = units.length; i < need; i++) w.appendChild(first.cloneNode(true)); |
| units = w.querySelectorAll('.lib-unit'); |
| for (let i = units.length - 1; i >= need; i--) units[i].remove(); |
| }); |
| } |
| |
| // ── Force the Gradio upload zone to English (it localises to the browser) ── |
| function enforceEnglishUpload() { |
| const row = document.querySelector('#upload-row'); |
| if (!row || !/D\\u00e9posez|Cliquez pour|Glissez/.test(row.textContent)) return; |
| const tw = document.createTreeWalker(row, NodeFilter.SHOW_TEXT); |
| let n; |
| while ((n = tw.nextNode())) { |
| let t = n.nodeValue; |
| t = t.replace(/(D\\u00e9posez|Glissez-d\\u00e9posez) le fichier ici/g, 'Drop your PDF here'); |
| t = t.replace(/-\\s*ou\\s*-/g, '- or -'); |
| t = t.replace(/Cliquez pour t\\u00e9l\\u00e9(charger|verser) un fichier/g, 'Click to upload'); |
| if (t !== n.nodeValue) n.nodeValue = t; |
| } |
| } |
| |
| // ── Background particles (canvas is in gr.HTML markup; animation here) ──── |
| function initParticles() { |
| const canvas = $('particles'); |
| if (!canvas || canvas._pp) return; |
| canvas._pp = true; |
| const ctx = canvas.getContext('2d'); |
| let W, H; |
| const rand = (a, b) => a + Math.random() * (b - a); |
| function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; } |
| window.addEventListener('resize', resize); |
| resize(); |
| const parts = Array.from({length: 65}, () => ({ |
| x: rand(0, W), y: rand(0, H), |
| vx: rand(-.25, .25), vy: rand(-.25, .25), |
| r: rand(1, 2.2), |
| purple: Math.random() > .5, |
| alpha: rand(.15, .55), |
| })); |
| (function draw() { |
| ctx.clearRect(0, 0, W, H); |
| for (let i = 0; i < parts.length; i++) { |
| const p = parts[i]; |
| p.x += p.vx; p.y += p.vy; |
| if (p.x < -5) p.x = W + 5; if (p.x > W + 5) p.x = -5; |
| if (p.y < -5) p.y = H + 5; if (p.y > H + 5) p.y = -5; |
| for (let j = i + 1; j < parts.length; j++) { |
| const q = parts[j]; |
| const dx = p.x - q.x, dy = p.y - q.y; |
| const dist = Math.hypot(dx, dy); |
| if (dist < 110) { |
| ctx.beginPath(); |
| ctx.moveTo(p.x, p.y); ctx.lineTo(q.x, q.y); |
| ctx.strokeStyle = 'rgba(124,58,237,' + ((1 - dist / 110) * 0.12) + ')'; |
| ctx.lineWidth = 1; |
| ctx.stroke(); |
| } |
| } |
| ctx.beginPath(); |
| ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); |
| ctx.fillStyle = 'rgba(' + (p.purple ? '124,58,237' : '6,182,212') + ',' + p.alpha + ')'; |
| ctx.fill(); |
| } |
| requestAnimationFrame(draw); |
| })(); |
| } |
| |
| // ── Score ring ───────────────────────────────────────────────────────────── |
| function updateScore() { |
| const arc = $('score-arc'); |
| const label = $('score-label'); |
| if (!arc || !label) return; |
| const n = S.correct, d = S.total; |
| label.textContent = d === 0 ? '—' : n + '/' + d; |
| arc.style.strokeDashoffset = 138.2 * (1 - (d ? n/d : 0)); |
| arc.style.stroke = d === 0 ? 'url(#scoreGrad)' : (n/d >= .7 ? '#10B981' : n/d >= .4 ? '#F59E0B' : '#EF4444'); |
| } |
| |
| // ── Feedback rendering ───────────────────────────────────────────────────── |
| function extractVerdict(t) { |
| const lo = t.toLowerCase(), lines = lo.split('\\n'); |
| for (const l of lines) { |
| if (l.includes('verdict')) { |
| if (l.includes('partially')) return 'partial'; |
| if (l.includes('incorrect')) return 'incorrect'; |
| if (l.includes('correct')) return 'correct'; |
| } |
| } |
| const h = lo.slice(0,200); |
| return h.includes('incorrect') ? 'incorrect' : h.includes('partially') ? 'partial' : h.includes('correct') ? 'correct' : 'unknown'; |
| } |
| |
| function showFeedback(text) { |
| const badge = $('verdict-badge'), fc = $('feedback-card'), body = $('feedback-body'); |
| const verdict = extractVerdict(text); |
| if (badge) { |
| badge.className = 'verdict-badge ' + (verdict === 'unknown' ? '' : verdict); |
| badge.textContent = verdict === 'correct' ? '✓ Correct' : verdict === 'partial' ? '~ Partially Correct' : verdict === 'incorrect' ? '✗ Incorrect' : '— Unknown'; |
| } |
| if (body) { |
| const labels = ['','Verdict','What was good','What was missing','Model answer']; |
| const sections = [], re = /(\\d+)\\.\\s*([^:\\n]*)[::]?\\s*([\\s\\S]*?)(?=\\n\\d+\\.|$)/g; |
| let m; |
| while ((m = re.exec(text)) !== null) sections.push({num:+m[1], body:m[3].trim().replace(/^\\d+\\.\\s*/,'')}); |
| body.innerHTML = sections.length >= 2 |
| ? sections.map(s => '<div class="section"><span class="section-num">'+(labels[s.num]||'Part '+s.num)+'</span><div class="section-content">'+s.body.replace(/\\n/g,'<br>')+'</div></div>').join('') |
| : '<div class="feedback-raw">'+text.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')+'</div>'; |
| } |
| const chunkEl = document.querySelector('#hidden-chunk-output textarea'); |
| const srcEl = document.getElementById('source-text'); |
| if (srcEl && chunkEl) srcEl.textContent = chunkEl.value; |
| if (fc) { fc.classList.remove('hidden'); fc.classList.add('fade-in-up'); setTimeout(() => fc.scrollIntoView({behavior:'smooth',block:'nearest'}),100); } |
| return verdict; |
| } |
| |
| // ── Session reward image (generic encouraging illustration) ─────────────── |
| // Pre-generated in the background as soon as the first question shows, so |
| // the End Session modal can display it instantly. |
| function requestSessionImage() { |
| if (S.imgRequested) return; |
| if (!setGradioTA('#hidden-session-topics textarea', 'study session')) return; |
| S.imgRequested = true; |
| // Small delay so Gradio registers the input before the click |
| setTimeout(() => clickGradioBtn('#hidden-generate-session-image-btn'), 200); |
| } |
| |
| function stopSessionImagePoll() { |
| if (S.imgPoll) { clearInterval(S.imgPoll); S.imgPoll = null; } |
| $('modal-image-loading')?.classList.add('hidden'); |
| } |
| |
| function showSessionImage() { |
| const sec = $('modal-session-image'), loading = $('modal-image-loading'), img = $('session-image'); |
| if (!sec || !loading || !img) return; |
| stopSessionImagePoll(); |
| sec.classList.add('hidden'); sec.classList.remove('fade-in-up'); |
| const ready = document.querySelector('#hidden-concept-image img'); |
| if (ready && ready.src) { // preloaded — show instantly |
| img.src = ready.src; |
| sec.classList.remove('hidden'); |
| return; |
| } |
| requestSessionImage(); // fallback in case the preload never fired |
| loading.classList.remove('hidden'); |
| const started = Date.now(); |
| S.imgPoll = setInterval(() => { |
| const el = document.querySelector('#hidden-concept-image img'); |
| if (el && el.src) { |
| stopSessionImagePoll(); |
| img.src = el.src; |
| sec.classList.remove('hidden'); |
| void sec.offsetWidth; // restart fade-in animation |
| sec.classList.add('fade-in-up'); |
| } else if (Date.now() - started > 120000) { |
| stopSessionImagePoll(); // give up quietly after 2 min |
| } |
| }, 500); |
| } |
| |
| // ── Question display ─────────────────────────────────────────────────────── |
| function showQuestion(text) { |
| S.currentQuestion = text; S.waitingQ = false; |
| const es = $('empty-state'), qa = $('question-area'), qt = $('q-text'), ql = $('q-loading'); |
| const eb = $('end-btn'), fc = $('feedback-card'), ai = $('answer-input'), cc = $('char-count'); |
| const sb = $('submit-btn'), nq = $('new-q-btn'); |
| if (es) es.classList.add('hidden'); |
| if (qa) qa.classList.remove('hidden'); |
| if (eb) eb.classList.remove('hidden'); |
| if (ql) ql.classList.add('hidden'); |
| if (qt) qt.textContent = text; |
| if (fc) fc.classList.add('hidden'); |
| if (ai) { ai.value = ''; ai.disabled = false; } |
| if (cc) cc.textContent = '0 chars'; |
| if (sb) sb.disabled = true; |
| if (nq) nq.disabled = false; |
| $('mcq-options')?.classList.add('hidden'); |
| $('answer-wrapper')?.classList.remove('hidden'); |
| requestSessionImage(); // generate the reward image while the user answers |
| } |
| |
| // ── Language toggle ──────────────────────────────────────────────────────── |
| function setLanguage(lang) { |
| setGradioTA('#hidden-language-box textarea', lang); |
| $('lang-fr-btn')?.classList.toggle('active', lang === 'Français'); |
| $('lang-en-btn')?.classList.toggle('active', lang === 'English'); |
| } |
| |
| // ── Mode toggle ──────────────────────────────────────────────────────────── |
| function setMode(m) { |
| S.mode = m; |
| $('mode-open-btn')?.classList.toggle('selected', m === 'open'); |
| $('mode-mcq-btn')?.classList.toggle('selected', m === 'mcq'); |
| S.currentQuestion = ''; S.mcqData = null; |
| $('empty-state')?.classList.remove('hidden'); |
| $('question-area')?.classList.add('hidden'); |
| $('feedback-card')?.classList.add('hidden'); |
| } |
| |
| // ── MCQ display ──────────────────────────────────────────────────────────── |
| function showMCQ(data) { |
| S.mcqData = data; S.waitingMCQ = false; S.mcqAnswered = false; |
| const qt = $('q-text'), ql = $('q-loading'), nq = $('new-q-btn'); |
| const es = $('empty-state'), qa = $('question-area'), eb = $('end-btn'); |
| if (es) es.classList.add('hidden'); |
| if (qa) qa.classList.remove('hidden'); |
| if (eb) eb.classList.remove('hidden'); |
| if (ql) ql.classList.add('hidden'); |
| if (qt) qt.textContent = data.question || ''; |
| if (nq) nq.disabled = false; |
| for (const l of ['A','B','C','D']) { |
| const txt = $('mcq-text-'+l.toLowerCase()); |
| const btn = $('mcq-'+l.toLowerCase()); |
| if (txt) txt.textContent = (data.choices && data.choices[l]) || ''; |
| if (btn) { |
| btn.className = 'mcq-btn'; btn.disabled = false; |
| btn.onclick = ((letter) => () => handleMCQAnswer(letter))(l); |
| } |
| } |
| $('mcq-options')?.classList.remove('hidden'); |
| requestSessionImage(); // generate the reward image while the user answers |
| $('mcq-explanations')?.classList.add('hidden'); |
| $('answer-wrapper')?.classList.add('hidden'); |
| $('feedback-card')?.classList.add('hidden'); |
| } |
| |
| // ── MCQ answer handler (client-side — no LLM call needed) ───────────────── |
| function handleMCQAnswer(letter) { |
| const data = S.mcqData; |
| if (!data || !data.correct || S.waitingMCQ || S.mcqAnswered) return; |
| S.mcqAnswered = true; |
| const correct = data.correct; |
| const isCorrect = letter === correct; |
| for (const l of ['A','B','C','D']) { |
| const btn = $('mcq-'+l.toLowerCase()); |
| if (!btn) continue; |
| btn.disabled = true; |
| if (l === correct) btn.classList.add('mcq-correct'); |
| else if (l === letter) btn.classList.add('mcq-wrong'); |
| else btn.classList.add('mcq-neutral'); |
| } |
| const expEl = $('mcq-explanations'); |
| if (expEl && data.explanations) { |
| expEl.innerHTML = ['A','B','C','D'].map(l => { |
| const exp = data.explanations[l]; if (!exp) return ''; |
| const cls = l===correct ? 'exp-correct' : l===letter ? 'exp-wrong' : 'exp-neutral'; |
| return '<div class="mcq-exp '+cls+'"><span class="mcq-exp-letter">'+l+'</span><span class="mcq-exp-text">'+exp+'</span></div>'; |
| }).join(''); |
| expEl.classList.remove('hidden'); |
| } |
| S.total++; if (isCorrect) S.correct++; |
| S.currentQuestion = data.question || ''; |
| S.history.push({question: S.currentQuestion, verdict: isCorrect ? 'correct' : 'incorrect'}); |
| updateScore(); |
| const chunkEl = document.querySelector('#hidden-chunk-output textarea'); |
| const srcEl = document.getElementById('source-text'); |
| if (srcEl && chunkEl) srcEl.textContent = chunkEl.value; |
| const badge=$('verdict-badge'), fc=$('feedback-card'), body=$('feedback-body'); |
| if (badge) { |
| badge.className = 'verdict-badge '+(isCorrect?'correct':'incorrect'); |
| badge.textContent = isCorrect ? '✓ Correct' : '✗ Incorrect'; |
| } |
| if (body) body.innerHTML = ''; |
| if (fc) { fc.classList.remove('hidden'); fc.classList.add('fade-in-up'); setTimeout(()=>fc.scrollIntoView({behavior:'smooth',block:'nearest'}),100); } |
| } |
| |
| // ── Trigger actions via hidden Gradio components ─────────────────────────── |
| function fetchQuestion() { |
| if (S.waitingQ || S.waitingMCQ) return; |
| S.waitingQStart = Date.now(); |
| const ql = $('q-loading'), qm = $('q-loading-msg'), nq = $('new-q-btn'), fc = $('feedback-card'); |
| const es = $('empty-state'), qa = $('question-area'), eb = $('end-btn'); |
| if (ql) ql.classList.remove('hidden'); |
| if (qm) qm.textContent = S.total === 0 ? 'Model loading (~30–60s first time)…' : (S.mode==='mcq' ? 'Generating MCQ…' : 'Generating question…'); |
| if (nq) nq.disabled = true; |
| if (fc) fc.classList.add('hidden'); |
| if (es) es.classList.add('hidden'); |
| if (qa) qa.classList.remove('hidden'); |
| if (eb) eb.classList.remove('hidden'); |
| if (S.mode === 'mcq') { |
| S.waitingMCQ = true; |
| S.prevMCQ = document.querySelector('#hidden-mcq-output textarea')?.value || ''; |
| const clicked = setGradioTA('#hidden-mcq-trigger textarea', Date.now().toString()); |
| if (!clicked) applyStatus('⚠️ Could not reach Gradio backend — try reloading the page.'); |
| } else { |
| S.waitingQ = true; |
| S.prevQuestion = document.querySelector('#hidden-question-output textarea')?.value || ''; |
| const clicked = setGradioTA('#hidden-question-trigger textarea', Date.now().toString()); |
| if (!clicked) applyStatus('⚠️ Could not reach Gradio backend — try reloading the page.'); |
| } |
| } |
| |
| function doSubmit() { |
| const ai = $('answer-input'); |
| const answer = ai?.value?.trim(); |
| if (!answer || !S.currentQuestion || S.waitingFB) return; |
| S.waitingFB = true; |
| S.prevFeedback = document.querySelector('#hidden-feedback-output textarea')?.value || ''; |
| const sb = $('submit-btn'), st = $('submit-btn-text'), ss = $('submit-spinner'), nq = $('new-q-btn'); |
| if (sb) sb.disabled = true; |
| if (st) st.textContent = 'Evaluating…'; |
| if (ss) ss.classList.remove('hidden'); |
| if (nq) nq.disabled = true; |
| setGradioTA('#hidden-answer-input textarea', answer); |
| setTimeout(() => setGradioTA('#hidden-answer-trigger textarea', Date.now().toString()), 200); |
| } |
| |
| // ── Modal ────────────────────────────────────────────────────────────────── |
| function showModal() { |
| const modal = $('modal'); |
| if (!modal) return; |
| const pct = S.total > 0 ? Math.round(S.correct/S.total*100) : 0; |
| const msb = $('modal-score-big'), msl = $('modal-score-label'), mm = $('modal-message'), mh = $('modal-history'); |
| if (msb) msb.textContent = S.correct+'/'+S.total; |
| if (msl) msl.textContent = S.total === 1 ? 'question answered' : 'questions answered'; |
| if (mm) mm.textContent = pct>=90 ? "Outstanding! You've mastered this material. \\uD83C\\uDF1F" |
| : pct>=70 ? "Great work — solid grasp of the content. \\uD83D\\uDCAA" |
| : pct>=50 ? "Good start! Keep practising the tricky sections. \\uD83D\\uDCD6" |
| : "Keep going — each question makes you stronger. \\uD83D\\uDD25"; |
| if (mh && S.history.length > 0) { |
| mh.classList.remove('hidden'); |
| mh.innerHTML = S.history.slice(-8).map(h => { |
| const vc = h.verdict==='correct'?'correct':h.verdict==='partial'?'partial':'incorrect'; |
| return '<div class="modal-history-item"><span class="hist-badge '+vc+'">'+(vc==='correct'?'✓':vc==='partial'?'~':'✗')+'</span><span class="hist-q">'+h.question+'</span></div>'; |
| }).join(''); |
| } |
| showSessionImage(); |
| modal.classList.remove('hidden'); |
| } |
| |
| // ── Wire HTML buttons ────────────────────────────────────────────────────── |
| function wireButtons() { |
| const pairs = [ |
| ['start-btn', () => fetchQuestion()], |
| ['new-q-btn', () => fetchQuestion()], |
| ['submit-btn', () => doSubmit()], |
| ['mode-open-btn',() => setMode('open')], |
| ['mode-mcq-btn', () => setMode('mcq')], |
| ['lang-fr-btn', () => setLanguage('Français')], |
| ['lang-en-btn', () => setLanguage('English')], |
| ['end-btn', () => { if (S.total===0) { const m=$('modal'); if(m) m.classList.remove('hidden'); } else showModal(); }], |
| ['modal-close', () => { |
| const modal=$('modal'); if(modal) modal.classList.add('hidden'); |
| S.correct=0; S.total=0; S.history=[]; S.currentQuestion=''; S.mcqData=null; |
| updateScore(); |
| const ai=$('answer-input'), cc=$('char-count'), sb=$('submit-btn'), fc=$('feedback-card'); |
| const es=$('empty-state'), qa=$('question-area'), eb=$('end-btn'), qc=$('quiz-card'); |
| if(ai){ai.value='';} if(cc) cc.textContent='0 chars'; if(sb) sb.disabled=true; |
| if(fc) fc.classList.add('hidden'); if(es) es.classList.remove('hidden'); |
| if(qa) qa.classList.add('hidden'); if(eb) eb.classList.add('hidden'); |
| stopSessionImagePoll(); |
| $('modal-session-image')?.classList.add('hidden'); |
| if(qc) qc.scrollIntoView({behavior:'smooth',block:'start'}); |
| }], |
| ]; |
| for (const [id, fn] of pairs) { |
| const el = $(id); |
| if (el && !el._pp) { el._pp = true; el.addEventListener('click', fn); } |
| } |
| const modal = $('modal'); |
| if (modal && !modal._pp) { modal._pp = true; modal.addEventListener('click', e => { if(e.target===modal) { modal.classList.add('hidden'); stopSessionImagePoll(); } }); } |
| const ai = $('answer-input'), sb = $('submit-btn'), cc = $('char-count'); |
| if (ai && !ai._pp) { |
| ai._pp = true; |
| ai.addEventListener('input', () => { |
| if(cc) cc.textContent = ai.value.length+' chars'; |
| if(sb) sb.disabled = ai.value.trim().length===0 || !S.currentQuestion; |
| }); |
| } |
| } |
| |
| // ── Main polling loop ────────────────────────────────────────────────────── |
| setInterval(function() { |
| wireButtons(); |
| initHeroProfessor(); |
| initParticles(); |
| syncLibraryWings(); |
| enforceEnglishUpload(); |
| |
| const statusEl = document.querySelector('#hidden-status-output textarea'); |
| if (statusEl && statusEl.value) applyStatus(statusEl.value); |
| |
| // Question output — picked up even after a timeout (CPU runtime can be |
| // slower than the spinner's patience; a late result is still a result). |
| { |
| const qEl = document.querySelector('#hidden-question-output textarea'); |
| if (qEl && qEl.value && qEl.value !== S.prevQuestion) { |
| const val = qEl.value; |
| S.prevQuestion = val; |
| const isErr = val.startsWith('Erreur') || val.startsWith('⚠️'); |
| if (isErr) { |
| S.waitingQ = false; |
| const ql=$('q-loading'), nq=$('new-q-btn'); |
| if(ql) ql.classList.add('hidden'); |
| if(nq) nq.disabled=false; |
| applyStatus(val); |
| } else if (S.mode === 'open' && (S.waitingQ || S.quizRevealed)) { |
| showQuestion(val); |
| } |
| } |
| } |
| |
| if (S.waitingQ) { |
| const elapsed = Math.round((Date.now() - S.waitingQStart) / 1000); |
| const qm = $('q-loading-msg'); |
| if (qm) { |
| if (elapsed < 15) qm.textContent = 'Generating question…'; |
| else if (elapsed < 90) qm.textContent = 'Model working… (' + elapsed + 's)'; |
| else if (elapsed < 240) qm.textContent = 'Still working… (' + elapsed + 's) — CPU runtime can take a few minutes'; |
| else qm.textContent = 'Very slow… (' + elapsed + 's) — check Space logs if this persists'; |
| } |
| // Hard timeout: 5 min — unlock UI (a late result will still display) |
| if (elapsed > 300) { |
| S.waitingQ = false; |
| const ql=$('q-loading'), nq=$('new-q-btn'); |
| if(ql) ql.classList.add('hidden'); |
| if(nq) nq.disabled=false; |
| applyStatus('⚠️ Request timed out (' + elapsed + 's). The question will appear if it finishes — or try again.'); |
| } |
| } |
| |
| // MCQ output — same late-pickup logic as questions. |
| { |
| const mcqEl = document.querySelector('#hidden-mcq-output textarea'); |
| if (mcqEl && mcqEl.value && mcqEl.value !== S.prevMCQ) { |
| S.prevMCQ = mcqEl.value; |
| try { |
| const data = JSON.parse(mcqEl.value); |
| if (data && data.question && S.mode === 'mcq' && (S.waitingMCQ || S.quizRevealed)) showMCQ(data); |
| else if (S.waitingMCQ) { S.waitingMCQ=false; $('q-loading')?.classList.add('hidden'); $('new-q-btn').disabled=false; applyStatus('⚠️ MCQ generation failed.'); } |
| } catch(e) { if (S.waitingMCQ) { S.waitingMCQ=false; $('q-loading')?.classList.add('hidden'); $('new-q-btn').disabled=false; } } |
| } |
| } |
| |
| if (S.waitingMCQ) { |
| const elapsed = Math.round((Date.now() - S.waitingQStart) / 1000); |
| const qm = $('q-loading-msg'); |
| if (qm) { |
| if (elapsed<15) qm.textContent='Generating MCQ…'; |
| else if (elapsed<90) qm.textContent='Model working… ('+elapsed+'s)'; |
| else if (elapsed<240) qm.textContent='Still working… ('+elapsed+'s) — CPU runtime can take a few minutes'; |
| else qm.textContent='Very slow… ('+elapsed+'s) — check Space logs'; |
| } |
| if (elapsed>300) { |
| S.waitingMCQ=false; |
| $('q-loading')?.classList.add('hidden'); $('new-q-btn').disabled=false; |
| applyStatus('⚠️ MCQ timed out ('+elapsed+'s). It will appear if it finishes — or try again.'); |
| } |
| } |
| |
| if (S.waitingFB) { |
| const fbEl = document.querySelector('#hidden-feedback-output textarea'); |
| if (fbEl && fbEl.value && fbEl.value !== S.prevFeedback) { |
| const text = fbEl.value; |
| S.prevFeedback = text; S.waitingFB = false; |
| const sb=$('submit-btn'), st=$('submit-btn-text'), ss=$('submit-spinner'), nq=$('new-q-btn'); |
| if(st) st.textContent='Submit Answer'; if(ss) ss.classList.add('hidden'); |
| if(sb) sb.disabled=false; if(nq) nq.disabled=false; |
| const verdict = showFeedback(text); |
| S.total++; |
| if (verdict==='correct') S.correct++; |
| S.history.push({question:S.currentQuestion, verdict}); |
| updateScore(); |
| } |
| } |
| |
| }, 300); |
| }""" |
|
|
| print("✅ Starting Gradio app...") |
| with gr.Blocks(title="PaperProf", css=CSS) as demo: |
|
|
| gr.HTML(CUSTOM_HTML) |
| demo.load(None, js=BRIDGE_JS) |
|
|
| chunks_state = gr.State([]) |
| chunk_state = gr.State("") |
| correct_state = gr.State(0) |
| total_state = gr.State(0) |
|
|
| with gr.Row(elem_id="upload-row"): |
| pdf_input = gr.File(label="📎 Cours PDF", file_types=[".pdf"], scale=3) |
| load_btn = gr.Button("Load PDF", variant="primary", scale=2, elem_id="hidden-load-btn") |
| language_selector = gr.Radio(["English", "Français"], value="English", label="🌐 Language", visible=False) |
|
|
| with gr.Row(elem_id="hidden-row-status"): |
| load_status = gr.Textbox(label="Status", interactive=False, scale=4, elem_id="hidden-status-output") |
| score_box = gr.Textbox(label="Score", value="—", interactive=False, scale=1, elem_id="score-box", visible=False) |
|
|
| with gr.Row(elem_id="hidden-row-question"): |
| question_box = gr.Textbox(label="❓ Question", interactive=False, lines=3, scale=3, elem_id="hidden-question-output") |
| new_q_btn = gr.Button("New Question", variant="secondary", scale=1, elem_id="hidden-question-btn") |
|
|
| answer_box = gr.Textbox(label="✏️ Your Answer", lines=4, placeholder="Write your answer here…", elem_id="hidden-answer-input") |
| submit_btn = gr.Button("Submit Answer", variant="primary", elem_id="hidden-submit-btn") |
| feedback_box = gr.Textbox(label="💬 Feedback", interactive=False, lines=7, elem_id="hidden-feedback-output") |
| concept_image = gr.Image(label="concept", type="pil", interactive=False, visible=True, elem_id="hidden-concept-image") |
| question_trigger = gr.Textbox(value="0", label="qtrig", elem_id="hidden-question-trigger") |
| answer_trigger = gr.Textbox(value="0", label="atrig", elem_id="hidden-answer-trigger") |
| chunk_display = gr.Textbox(value="", label="chunk", elem_id="hidden-chunk-output") |
| mcq_trigger = gr.Textbox(value="0", label="mcqtrig", elem_id="hidden-mcq-trigger") |
| mcq_output = gr.Textbox(value="", label="mcqout", elem_id="hidden-mcq-output") |
| language_box = gr.Textbox(value="English", label="lang", elem_id="hidden-language-box") |
| session_topics = gr.Textbox(value="", label="topics", elem_id="hidden-session-topics") |
| session_img_btn = gr.Button("gen session image", elem_id="hidden-generate-session-image-btn") |
|
|
| load_btn.click( |
| load_pdf, |
| inputs=[pdf_input, language_selector], |
| outputs=[chunks_state, load_status, correct_state, total_state], |
| ).then(lambda: "—", outputs=[score_box]) |
|
|
| question_trigger.change( |
| new_question, |
| inputs=[chunks_state, language_box], |
| outputs=[question_box, chunk_state, feedback_box, chunk_display], |
| ) |
|
|
| answer_trigger.change( |
| submit_answer, |
| inputs=[question_box, chunk_state, answer_box, correct_state, total_state, language_box], |
| outputs=[feedback_box, correct_state, total_state, score_box], |
| ) |
|
|
| session_img_btn.click( |
| generate_image_fn, |
| inputs=[session_topics], |
| outputs=[concept_image], |
| ) |
|
|
| mcq_trigger.change( |
| new_mcq, |
| inputs=[chunks_state, language_box], |
| outputs=[question_box, chunk_state, mcq_output, chunk_display], |
| ) |
|
|
| print("✅ Gradio demo defined successfully") |
|
|
| print("✅ Launching demo...") |
| demo.launch(server_name="0.0.0.0", server_port=7860) |
|
|