Ryadg commited on
Commit
12edc45
Β·
1 Parent(s): f2b3289

fix: full quiz flow via BRIDGE_JS, English only, buttons DOM-visible

Browse files
Files changed (2) hide show
  1. app.py +242 -58
  2. ui/index.html +9 -40
app.py CHANGED
@@ -302,65 +302,249 @@ def submit_answer(question, chunk, student_answer, correct, total, language):
302
  # ---------------------------------------------------------------------------
303
 
304
  BRIDGE_JS = """() => {
305
- let lastStatus = '';
306
- let quizRevealed = false;
307
-
308
- function ensureStatusBar() {
309
- if (document.getElementById('gradio-status-bar')) return;
310
- const anchor = document.getElementById('quiz-card') || document.getElementById('app');
311
- if (!anchor) return;
312
- const bar = document.createElement('div');
313
- bar.id = 'gradio-status-bar';
314
- 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;';
315
- anchor.parentNode.insertBefore(bar, anchor);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
 
318
- function applyStatus(text) {
319
- if (text === lastStatus) return;
320
- lastStatus = text;
321
- ensureStatusBar();
322
- const bar = document.getElementById('gradio-status-bar');
323
- if (!bar) return;
324
- bar.textContent = text;
325
- bar.style.display = 'block';
326
- const isError = text.includes('\\u26a0') || text.toLowerCase().includes('erreur');
327
- const isSuccess = text.includes('sections') || text.includes('extraites') || text.includes('extracted') || text.includes('\\ud83d\\udcda');
328
- if (isError) {
329
- bar.style.background = 'rgba(239,68,68,0.12)';
330
- bar.style.border = '1px solid rgba(239,68,68,0.3)';
331
- bar.style.color = '#FCA5A5';
332
- } else if (isSuccess) {
333
- bar.style.background = 'rgba(16,185,129,0.12)';
334
- bar.style.border = '1px solid rgba(16,185,129,0.3)';
335
- bar.style.color = '#6EE7B7';
336
- if (!quizRevealed) {
337
- quizRevealed = true;
338
- const quizCard = document.getElementById('quiz-card');
339
- if (quizCard) {
340
- quizCard.classList.remove('hidden');
341
- setTimeout(() => quizCard.scrollIntoView({ behavior: 'smooth', block: 'start' }), 400);
342
- }
343
- const uploadRow = document.getElementById('upload-row');
344
- if (uploadRow) {
345
- uploadRow.style.opacity = '0.5';
346
- uploadRow.style.transform = 'scale(0.97)';
347
- uploadRow.style.transition = 'all .4s ease';
348
- uploadRow.style.pointerEvents = 'none';
349
- }
350
- }
351
- } else {
352
- bar.style.background = 'rgba(6,182,212,0.12)';
353
- bar.style.border = '1px solid rgba(6,182,212,0.3)';
354
- bar.style.color = '#67E8F9';
355
- }
356
  }
357
 
358
- // Poll every 300 ms β€” Svelte sets textarea.value as a JS property
359
- // which characterData MutationObserver does not catch.
360
- setInterval(function() {
361
- const el = document.querySelector('#hidden-status-output textarea');
362
- if (el && el.value) applyStatus(el.value);
363
- }, 300);
 
 
 
 
 
 
 
 
 
 
364
  }"""
365
 
366
  print("βœ… Starting Gradio app...")
@@ -385,10 +569,10 @@ with gr.Blocks(title="PaperProf", css=CSS) as demo:
385
 
386
  with gr.Row(elem_id="hidden-row-question"):
387
  question_box = gr.Textbox(label="❓ Question", interactive=False, lines=3, scale=3, elem_id="hidden-question-output")
388
- new_q_btn = gr.Button("New Question", variant="secondary", scale=1, visible=False, elem_id="hidden-question-btn")
389
 
390
- answer_box = gr.Textbox(label="✏️ Your Answer", lines=4, placeholder="Write your answer here…", visible=False, elem_id="hidden-answer-input")
391
- submit_btn = gr.Button("Submit Answer", variant="primary", visible=False, elem_id="hidden-submit-btn")
392
  feedback_box = gr.Textbox(label="πŸ’¬ Feedback", interactive=False, lines=7, elem_id="hidden-feedback-output")
393
 
394
  # Traduction UI quand on change la langue
 
302
  # ---------------------------------------------------------------------------
303
 
304
  BRIDGE_JS = """() => {
305
+ // ── State ──────────────────────────────────────────────────────────────────
306
+ const S = {
307
+ correct: 0, total: 0, history: [],
308
+ currentQuestion: '',
309
+ quizRevealed: false,
310
+ lastStatus: '',
311
+ waitingQ: false, prevQuestion: '',
312
+ waitingFB: false, prevFeedback: '',
313
+ };
314
+ const $ = id => document.getElementById(id);
315
+
316
+ // ── Status bar ─────────────────────────────────────────────────────────────
317
+ function ensureStatusBar() {
318
+ if ($('gradio-status-bar')) return;
319
+ const anchor = $('quiz-card') || $('app');
320
+ if (!anchor) return;
321
+ const bar = document.createElement('div');
322
+ bar.id = 'gradio-status-bar';
323
+ 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;';
324
+ anchor.parentNode.insertBefore(bar, anchor);
325
+ }
326
+
327
+ function applyStatus(text) {
328
+ if (text === S.lastStatus) return;
329
+ S.lastStatus = text;
330
+ ensureStatusBar();
331
+ const bar = $('gradio-status-bar');
332
+ if (!bar) return;
333
+ bar.textContent = text;
334
+ bar.style.display = 'block';
335
+ const isErr = text.includes('\\u26a0') || text.toLowerCase().includes('erreur');
336
+ const isOK = text.includes('sections') || text.includes('extracted') || text.includes('extraites');
337
+ if (isErr) {
338
+ bar.style.cssText += ';background:rgba(239,68,68,.12);border:1px solid rgba(239,68,68,.3);color:#FCA5A5';
339
+ } else if (isOK) {
340
+ bar.style.cssText += ';background:rgba(16,185,129,.12);border:1px solid rgba(16,185,129,.3);color:#6EE7B7';
341
+ if (!S.quizRevealed) { S.quizRevealed = true; revealQuiz(); }
342
+ } else {
343
+ bar.style.cssText += ';background:rgba(6,182,212,.12);border:1px solid rgba(6,182,212,.3);color:#67E8F9';
344
  }
345
+ }
346
+
347
+ function revealQuiz() {
348
+ const qc = $('quiz-card');
349
+ if (qc) { qc.classList.remove('hidden'); setTimeout(() => qc.scrollIntoView({behavior:'smooth',block:'start'}), 400); }
350
+ const ur = document.querySelector('#upload-row');
351
+ if (ur) { ur.style.cssText += ';opacity:0;height:0;overflow:hidden;margin:0;padding:0;pointer-events:none;transition:all .4s ease'; }
352
+ }
353
+
354
+ // ── Helpers ────────────────────────────────────────────────────────────────
355
+ function setGradioTA(sel, val) {
356
+ const el = document.querySelector(sel);
357
+ if (!el) return false;
358
+ Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,'value').set.call(el, val);
359
+ el.dispatchEvent(new Event('input', {bubbles:true}));
360
+ el.dispatchEvent(new Event('change', {bubbles:true}));
361
+ return true;
362
+ }
363
+ function clickGradioBtn(sel) {
364
+ const btn = document.querySelector(sel + ' button');
365
+ if (btn) { btn.click(); return true; } return false;
366
+ }
367
+
368
+ // ── Score ring ─────────────────────────────────────────────────────────────
369
+ function updateScore() {
370
+ const arc = $('score-arc');
371
+ const label = $('score-label');
372
+ if (!arc || !label) return;
373
+ const n = S.correct, d = S.total;
374
+ label.textContent = d === 0 ? 'β€”' : n + '/' + d;
375
+ arc.style.strokeDashoffset = 138.2 * (1 - (d ? n/d : 0));
376
+ arc.style.stroke = d === 0 ? 'url(#scoreGrad)' : (n/d >= .7 ? '#10B981' : n/d >= .4 ? '#F59E0B' : '#EF4444');
377
+ }
378
+
379
+ // ── Feedback rendering ─────────────────────────────────────────────────────
380
+ function extractVerdict(t) {
381
+ const lo = t.toLowerCase(), lines = lo.split('\\n');
382
+ for (const l of lines) {
383
+ if (l.includes('verdict')) {
384
+ if (l.includes('partially')) return 'partial';
385
+ if (l.includes('incorrect')) return 'incorrect';
386
+ if (l.includes('correct')) return 'correct';
387
+ }
388
+ }
389
+ const h = lo.slice(0,200);
390
+ return h.includes('incorrect') ? 'incorrect' : h.includes('partially') ? 'partial' : h.includes('correct') ? 'correct' : 'unknown';
391
+ }
392
+
393
+ function showFeedback(text) {
394
+ const badge = $('verdict-badge'), fc = $('feedback-card'), body = $('feedback-body');
395
+ const verdict = extractVerdict(text);
396
+ if (badge) {
397
+ badge.className = 'verdict-badge ' + (verdict === 'unknown' ? '' : verdict);
398
+ badge.textContent = verdict === 'correct' ? 'βœ“ Correct' : verdict === 'partial' ? '~ Partially Correct' : verdict === 'incorrect' ? 'βœ— Incorrect' : 'β€” Unknown';
399
+ }
400
+ if (body) {
401
+ const labels = ['','Verdict','What was good','What was missing','Model answer'];
402
+ const sections = [], re = /(\\d+)\\.\\s*([^:\\n]*)[::]?\\s*([\\s\\S]*?)(?=\\n\\d+\\.|$)/g;
403
+ let m;
404
+ while ((m = re.exec(text)) !== null) sections.push({num:+m[1], body:m[3].trim()});
405
+ body.innerHTML = sections.length >= 2
406
+ ? 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('')
407
+ : '<div class="feedback-raw">'+text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</div>';
408
+ }
409
+ if (fc) { fc.classList.remove('hidden'); fc.classList.add('fade-in-up'); setTimeout(() => fc.scrollIntoView({behavior:'smooth',block:'nearest'}),100); }
410
+ return verdict;
411
+ }
412
+
413
+ // ── Question display ───────────────────────────────────────────────────────
414
+ function showQuestion(text) {
415
+ S.currentQuestion = text; S.waitingQ = false;
416
+ const es = $('empty-state'), qa = $('question-area'), qt = $('q-text'), ql = $('q-loading');
417
+ const eb = $('end-btn'), fc = $('feedback-card'), ai = $('answer-input'), cc = $('char-count');
418
+ const sb = $('submit-btn'), nq = $('new-q-btn');
419
+ if (es) es.classList.add('hidden');
420
+ if (qa) qa.classList.remove('hidden');
421
+ if (eb) eb.classList.remove('hidden');
422
+ if (ql) ql.classList.add('hidden');
423
+ if (qt) qt.textContent = text;
424
+ if (fc) fc.classList.add('hidden');
425
+ if (ai) { ai.value = ''; ai.disabled = false; }
426
+ if (cc) cc.textContent = '0 chars';
427
+ if (sb) sb.disabled = true;
428
+ if (nq) nq.disabled = false;
429
+ }
430
+
431
+ // ── Trigger actions via hidden Gradio components ───────────────────────────
432
+ function fetchQuestion() {
433
+ if (S.waitingQ) return;
434
+ S.waitingQ = true;
435
+ S.prevQuestion = document.querySelector('#hidden-question-output textarea')?.value || '';
436
+ const ql = $('q-loading'), qm = $('q-loading-msg'), nq = $('new-q-btn'), fc = $('feedback-card');
437
+ const es = $('empty-state'), qa = $('question-area'), eb = $('end-btn');
438
+ if (ql) ql.classList.remove('hidden');
439
+ if (qm) qm.textContent = S.total === 0 ? 'Generating question (model loading, ~30s first time)…' : 'Generating question…';
440
+ if (nq) nq.disabled = true;
441
+ if (fc) fc.classList.add('hidden');
442
+ if (es) es.classList.add('hidden');
443
+ if (qa) qa.classList.remove('hidden');
444
+ if (eb) eb.classList.remove('hidden');
445
+ clickGradioBtn('#hidden-question-btn');
446
+ }
447
+
448
+ function doSubmit() {
449
+ const ai = $('answer-input');
450
+ const answer = ai?.value?.trim();
451
+ if (!answer || !S.currentQuestion || S.waitingFB) return;
452
+ S.waitingFB = true;
453
+ S.prevFeedback = document.querySelector('#hidden-feedback-output textarea')?.value || '';
454
+ const sb = $('submit-btn'), st = $('submit-btn-text'), ss = $('submit-spinner'), nq = $('new-q-btn');
455
+ if (sb) sb.disabled = true;
456
+ if (st) st.textContent = 'Evaluating…';
457
+ if (ss) ss.classList.remove('hidden');
458
+ if (nq) nq.disabled = true;
459
+ setGradioTA('#hidden-answer-input textarea', answer);
460
+ setTimeout(() => clickGradioBtn('#hidden-submit-btn'), 200);
461
+ }
462
+
463
+ // ── Modal ──────────────────────────────────────────────────────────────────
464
+ function showModal() {
465
+ const modal = $('modal');
466
+ if (!modal) return;
467
+ const pct = S.total > 0 ? Math.round(S.correct/S.total*100) : 0;
468
+ const msb = $('modal-score-big'), msl = $('modal-score-label'), mm = $('modal-message'), mh = $('modal-history');
469
+ if (msb) msb.textContent = S.correct+'/'+S.total;
470
+ if (msl) msl.textContent = S.total === 1 ? 'question answered' : 'questions answered';
471
+ if (mm) mm.textContent = pct>=90 ? "Outstanding! You've mastered this material. \\uD83C\\uDF1F"
472
+ : pct>=70 ? "Great work β€” solid grasp of the content. \\uD83D\\uDCAA"
473
+ : pct>=50 ? "Good start! Keep practising the tricky sections. \\uD83D\\uDCD6"
474
+ : "Keep going β€” each question makes you stronger. \\uD83D\\uDD25";
475
+ if (mh && S.history.length > 0) {
476
+ mh.classList.remove('hidden');
477
+ mh.innerHTML = S.history.slice(-8).map(h => {
478
+ const vc = h.verdict==='correct'?'correct':h.verdict==='partial'?'partial':'incorrect';
479
+ return '<div class="modal-history-item"><span class="hist-badge '+vc+'">'+(vc==='correct'?'βœ“':vc==='partial'?'~':'βœ—')+'</span><span class="hist-q">'+h.question+'</span></div>';
480
+ }).join('');
481
+ }
482
+ modal.classList.remove('hidden');
483
+ }
484
+
485
+ // ── Wire HTML buttons ──────────────────────────────────────────────────────
486
+ function wireButtons() {
487
+ const pairs = [
488
+ ['start-btn', () => fetchQuestion()],
489
+ ['new-q-btn', () => fetchQuestion()],
490
+ ['submit-btn', () => doSubmit()],
491
+ ['end-btn', () => { if (S.total===0) { const m=$('modal'); if(m) m.classList.remove('hidden'); } else showModal(); }],
492
+ ['modal-close', () => {
493
+ const modal=$('modal'); if(modal) modal.classList.add('hidden');
494
+ S.correct=0; S.total=0; S.history=[]; S.currentQuestion='';
495
+ updateScore();
496
+ const ai=$('answer-input'), cc=$('char-count'), sb=$('submit-btn'), fc=$('feedback-card');
497
+ const es=$('empty-state'), qa=$('question-area'), eb=$('end-btn'), qc=$('quiz-card');
498
+ if(ai){ai.value='';} if(cc) cc.textContent='0 chars'; if(sb) sb.disabled=true;
499
+ if(fc) fc.classList.add('hidden'); if(es) es.classList.remove('hidden');
500
+ if(qa) qa.classList.add('hidden'); if(eb) eb.classList.add('hidden');
501
+ if(qc) qc.scrollIntoView({behavior:'smooth',block:'start'});
502
+ }],
503
+ ];
504
+ for (const [id, fn] of pairs) {
505
+ const el = $(id);
506
+ if (el && !el._pp) { el._pp = true; el.addEventListener('click', fn); }
507
+ }
508
+ const modal = $('modal');
509
+ if (modal && !modal._pp) { modal._pp = true; modal.addEventListener('click', e => { if(e.target===modal) modal.classList.add('hidden'); }); }
510
+ const ai = $('answer-input'), sb = $('submit-btn'), cc = $('char-count');
511
+ if (ai && !ai._pp) {
512
+ ai._pp = true;
513
+ ai.addEventListener('input', () => {
514
+ if(cc) cc.textContent = ai.value.length+' chars';
515
+ if(sb) sb.disabled = ai.value.trim().length===0 || !S.currentQuestion;
516
+ });
517
+ }
518
+ }
519
+
520
+ // ── Main polling loop ──────────────────────────────────────────────────────
521
+ setInterval(function() {
522
+ wireButtons();
523
 
524
+ const statusEl = document.querySelector('#hidden-status-output textarea');
525
+ if (statusEl && statusEl.value) applyStatus(statusEl.value);
526
+
527
+ if (S.waitingQ) {
528
+ const qEl = document.querySelector('#hidden-question-output textarea');
529
+ if (qEl && qEl.value && qEl.value !== S.prevQuestion) showQuestion(qEl.value);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530
  }
531
 
532
+ if (S.waitingFB) {
533
+ const fbEl = document.querySelector('#hidden-feedback-output textarea');
534
+ if (fbEl && fbEl.value && fbEl.value !== S.prevFeedback) {
535
+ const text = fbEl.value;
536
+ S.prevFeedback = text; S.waitingFB = false;
537
+ const sb=$('submit-btn'), st=$('submit-btn-text'), ss=$('submit-spinner'), nq=$('new-q-btn');
538
+ if(st) st.textContent='Submit Answer'; if(ss) ss.classList.add('hidden');
539
+ if(sb) sb.disabled=false; if(nq) nq.disabled=false;
540
+ const verdict = showFeedback(text);
541
+ S.total++;
542
+ if (verdict==='correct') S.correct++;
543
+ S.history.push({question:S.currentQuestion, verdict});
544
+ updateScore();
545
+ }
546
+ }
547
+ }, 300);
548
  }"""
549
 
550
  print("βœ… Starting Gradio app...")
 
569
 
570
  with gr.Row(elem_id="hidden-row-question"):
571
  question_box = gr.Textbox(label="❓ Question", interactive=False, lines=3, scale=3, elem_id="hidden-question-output")
572
+ new_q_btn = gr.Button("New Question", variant="secondary", scale=1, elem_id="hidden-question-btn")
573
 
574
+ answer_box = gr.Textbox(label="✏️ Your Answer", lines=4, placeholder="Write your answer here…", elem_id="hidden-answer-input")
575
+ submit_btn = gr.Button("Submit Answer", variant="primary", elem_id="hidden-submit-btn")
576
  feedback_box = gr.Textbox(label="πŸ’¬ Feedback", interactive=False, lines=7, elem_id="hidden-feedback-output")
577
 
578
  # Traduction UI quand on change la langue
ui/index.html CHANGED
@@ -342,25 +342,6 @@
342
 
343
  <!-- Controls row -->
344
  <div class="controls-row">
345
- <!-- Language toggle -->
346
- <div>
347
- <div class="diff-label">Language</div>
348
- <div class="toggle-group" id="lang-toggle">
349
- <button class="pill active" data-val="English">EN</button>
350
- <button class="pill" data-val="FranΓ§ais">FR</button>
351
- </div>
352
- </div>
353
-
354
- <!-- Difficulty toggle -->
355
- <div>
356
- <div class="diff-label">Difficulty</div>
357
- <div class="toggle-group" id="diff-toggle">
358
- <button class="pill" data-val="Easy">Easy</button>
359
- <button class="pill active" data-val="Normal">Normal</button>
360
- <button class="pill" data-val="Hard">Hard</button>
361
- </div>
362
- </div>
363
-
364
  <!-- Score ring -->
365
  <div class="score-wrap" style="margin-left:auto" title="Score">
366
  <svg class="score-ring" viewBox="0 0 56 56">
@@ -452,27 +433,14 @@
452
  </div>
453
 
454
  <script>
455
- // ───────────────────────────────────────────────────────────────────────────
456
- // State
457
- // ───────────────────────────────────────────────────────────────────────────
458
- const S = {
459
- chunks: [],
460
- currentChunk: '',
461
- currentQuestion: '',
462
- correct: 0,
463
- total: 0,
464
- language: 'English',
465
- difficulty: 'Normal',
466
- history: [],
467
- loaded: false,
468
- };
469
-
470
- // ───────────────────────────────────────────────────────────────────────────
471
- // DOM refs
472
- // ───────────────────────────────────────────────────────────────────────────
473
- const $ = id => document.getElementById(id);
474
-
475
- const quizCard = $('quiz-card');
476
  const emptyState = $('empty-state');
477
  const questionArea = $('question-area');
478
  const qText = $('q-text');
@@ -830,4 +798,5 @@
830
  }
831
  draw();
832
  })();
 
833
  </script>
 
342
 
343
  <!-- Controls row -->
344
  <div class="controls-row">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  <!-- Score ring -->
346
  <div class="score-wrap" style="margin-left:auto" title="Score">
347
  <svg class="score-ring" viewBox="0 0 56 56">
 
433
  </div>
434
 
435
  <script>
436
+ // Script intentionally left minimal β€” all quiz logic lives in BRIDGE_JS
437
+ // injected via demo.load(js=BRIDGE_JS) which actually executes.
438
+ // innerHTML-injected <script> tags are never run by browsers.
439
+
440
+ // (all logic removed β€” lives in BRIDGE_JS)
441
+ // placeholder to keep closing tag:
442
+ void 0;
443
+ /*
 
 
 
 
 
 
 
 
 
 
 
 
 
444
  const emptyState = $('empty-state');
445
  const questionArea = $('question-area');
446
  const qText = $('q-text');
 
798
  }
799
  draw();
800
  })();
801
+ */
802
  </script>