feat: hybrid approach native Gradio file upload styled with custom CSS
Browse files- app.py +38 -5
- ui/index.html +3 -173
app.py
CHANGED
|
@@ -40,10 +40,43 @@ body {
|
|
| 40 |
background: #0A0F1E !important;
|
| 41 |
margin: 0 !important;
|
| 42 |
}
|
| 43 |
-
/*
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
"""
|
| 48 |
|
| 49 |
import pathlib
|
|
@@ -313,8 +346,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
| 313 |
total_state = gr.State(0)
|
| 314 |
|
| 315 |
with gr.Row():
|
| 316 |
-
pdf_input = gr.File(label="π Cours PDF", file_types=[".pdf"], scale=3
|
| 317 |
-
load_btn = gr.Button("Load PDF", variant="primary", scale=2,
|
| 318 |
language_selector = gr.Radio(["English", "FranΓ§ais"], value="English", label="π Language", visible=False)
|
| 319 |
|
| 320 |
with gr.Row():
|
|
|
|
| 40 |
background: #0A0F1E !important;
|
| 41 |
margin: 0 !important;
|
| 42 |
}
|
| 43 |
+
/* Style native Gradio file upload to match dark theme */
|
| 44 |
+
[data-testid="upload-zone"],
|
| 45 |
+
.file-drop-zone,
|
| 46 |
+
.upload-container {
|
| 47 |
+
background: rgba(124,58,237,0.05) !important;
|
| 48 |
+
border: 2px dashed rgba(124,58,237,0.35) !important;
|
| 49 |
+
border-radius: 10px !important;
|
| 50 |
+
min-height: 120px !important;
|
| 51 |
+
color: rgba(255,255,255,0.6) !important;
|
| 52 |
}
|
| 53 |
+
[data-testid="upload-zone"]:hover,
|
| 54 |
+
.file-drop-zone:hover {
|
| 55 |
+
border-color: #06B6D4 !important;
|
| 56 |
+
background: rgba(6,182,212,0.04) !important;
|
| 57 |
+
box-shadow: 0 0 28px rgba(6,182,212,0.15) !important;
|
| 58 |
+
}
|
| 59 |
+
/* Style Load PDF button */
|
| 60 |
+
#hidden-load-btn button {
|
| 61 |
+
background: linear-gradient(135deg, #7C3AED, #06B6D4) !important;
|
| 62 |
+
border-radius: 50px !important;
|
| 63 |
+
color: white !important;
|
| 64 |
+
font-weight: 700 !important;
|
| 65 |
+
border: none !important;
|
| 66 |
+
box-shadow: 0 4px 20px rgba(124,58,237,0.35) !important;
|
| 67 |
+
padding: 12px 28px !important;
|
| 68 |
+
}
|
| 69 |
+
#hidden-load-btn button:hover {
|
| 70 |
+
box-shadow: 0 6px 28px rgba(124,58,237,0.5) !important;
|
| 71 |
+
}
|
| 72 |
+
/* File preview area */
|
| 73 |
+
.file-preview-holder, .file-name-wrapper {
|
| 74 |
+
color: #A78BFA !important;
|
| 75 |
+
background: rgba(124,58,237,0.07) !important;
|
| 76 |
+
border-radius: 8px !important;
|
| 77 |
+
}
|
| 78 |
+
/* Labels */
|
| 79 |
+
.block label span { color: rgba(255,255,255,0.6) !important; }
|
| 80 |
"""
|
| 81 |
|
| 82 |
import pathlib
|
|
|
|
| 346 |
total_state = gr.State(0)
|
| 347 |
|
| 348 |
with gr.Row():
|
| 349 |
+
pdf_input = gr.File(label="π Cours PDF", file_types=[".pdf"], scale=3)
|
| 350 |
+
load_btn = gr.Button("Load PDF", variant="primary", scale=2, elem_id="hidden-load-btn")
|
| 351 |
language_selector = gr.Radio(["English", "FranΓ§ais"], value="English", label="π Language", visible=False)
|
| 352 |
|
| 353 |
with gr.Row():
|
ui/index.html
CHANGED
|
@@ -94,38 +94,6 @@
|
|
| 94 |
}
|
| 95 |
.card:hover { border-color: rgba(255,255,255,.12); }
|
| 96 |
|
| 97 |
-
/* ββ Drop zone ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 98 |
-
.drop-zone {
|
| 99 |
-
border: 2px dashed rgba(124,58,237,.35);
|
| 100 |
-
border-radius: var(--r-sm);
|
| 101 |
-
padding: 40px 24px;
|
| 102 |
-
text-align: center;
|
| 103 |
-
cursor: pointer;
|
| 104 |
-
transition: border-color .2s, box-shadow .2s, background .2s;
|
| 105 |
-
position: relative;
|
| 106 |
-
}
|
| 107 |
-
.drop-zone:hover, .drop-zone.drag-over {
|
| 108 |
-
border-color: var(--cyan);
|
| 109 |
-
box-shadow: 0 0 28px rgba(6,182,212,.15);
|
| 110 |
-
background: rgba(6,182,212,.04);
|
| 111 |
-
}
|
| 112 |
-
.drop-zone.has-file {
|
| 113 |
-
border-color: var(--purple);
|
| 114 |
-
box-shadow: 0 0 28px rgba(124,58,237,.15);
|
| 115 |
-
background: rgba(124,58,237,.04);
|
| 116 |
-
}
|
| 117 |
-
.upload-icon { font-size: 2.4rem; display: block; margin-bottom: 12px;
|
| 118 |
-
transition: transform .2s; }
|
| 119 |
-
.drop-zone:hover .upload-icon { transform: translateY(-4px); }
|
| 120 |
-
.drop-title { font-weight: 600; font-size: 1rem; margin-bottom: 4px; }
|
| 121 |
-
.drop-hint { color: var(--muted); font-size: .85rem; margin-bottom: 16px; }
|
| 122 |
-
.file-name {
|
| 123 |
-
margin-top: 12px; font-size: .9rem; font-weight: 600;
|
| 124 |
-
color: var(--purple-l);
|
| 125 |
-
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
| 126 |
-
max-width: 280px; margin-inline: auto;
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
/* ββ Buttons ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 130 |
button { cursor: pointer; font-family: inherit; font-size: .95rem;
|
| 131 |
border: none; outline: none; transition: all .2s; }
|
|
@@ -156,17 +124,6 @@
|
|
| 156 |
}
|
| 157 |
.btn-ghost:disabled { opacity: .35; cursor: not-allowed; }
|
| 158 |
|
| 159 |
-
.btn-browse {
|
| 160 |
-
display: inline-block;
|
| 161 |
-
padding: 9px 22px; border-radius: 50px; font-weight: 600; font-size: .9rem;
|
| 162 |
-
border: 1px solid rgba(124,58,237,.5); color: var(--purple-l);
|
| 163 |
-
background: rgba(124,58,237,.08);
|
| 164 |
-
cursor: pointer; transition: all .2s;
|
| 165 |
-
}
|
| 166 |
-
.btn-browse:hover {
|
| 167 |
-
background: rgba(124,58,237,.18); border-color: var(--purple);
|
| 168 |
-
}
|
| 169 |
-
|
| 170 |
/* ββ Pill toggles βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 171 |
.toggle-group { display: flex; gap: 6px; }
|
| 172 |
|
|
@@ -182,19 +139,6 @@
|
|
| 182 |
box-shadow: 0 2px 12px rgba(124,58,237,.4);
|
| 183 |
}
|
| 184 |
|
| 185 |
-
/* ββ Status / alerts ββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 186 |
-
.status {
|
| 187 |
-
margin-top: 14px; padding: 11px 16px; border-radius: var(--r-sm);
|
| 188 |
-
font-size: .9rem; font-weight: 500; line-height: 1.4;
|
| 189 |
-
animation: fadeUp .3s ease both;
|
| 190 |
-
}
|
| 191 |
-
.status.info { background: rgba(6,182,212,.12); border: 1px solid rgba(6,182,212,.3); color: var(--cyan-l); }
|
| 192 |
-
.status.ok { background: rgba(16,185,129,.12); border: 1px solid rgba(16,185,129,.3); color: #6EE7B7; }
|
| 193 |
-
.status.error { background: rgba(239,68,68,.12); border: 1px solid rgba(239,68,68,.3); color: #FCA5A5; }
|
| 194 |
-
|
| 195 |
-
/* ββ Upload actions row βββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 196 |
-
.upload-actions { margin-top: 20px; display: flex; justify-content: center; }
|
| 197 |
-
|
| 198 |
/* ββ Quiz controls row βββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 199 |
.controls-row {
|
| 200 |
display: flex; align-items: center; flex-wrap: wrap; gap: 12px;
|
|
@@ -401,25 +345,6 @@
|
|
| 401 |
<p class="hero-sub">Upload your course. Get quizzed by AI.</p>
|
| 402 |
</header>
|
| 403 |
|
| 404 |
-
<!-- ββ Upload card ββββββββββββββββββββββββββββββββββββββββββββββββββββ -->
|
| 405 |
-
<div class="card fade-in-up" id="upload-card" style="animation-delay:.05s">
|
| 406 |
-
<div class="drop-zone" id="drop-zone">
|
| 407 |
-
<span class="upload-icon" id="upload-icon">π</span>
|
| 408 |
-
<p class="drop-title" id="drop-title">Drop your PDF here</p>
|
| 409 |
-
<p class="drop-hint">or</p>
|
| 410 |
-
<label class="btn-browse" for="file-input">Browse files</label>
|
| 411 |
-
<input type="file" id="file-input" accept=".pdf" style="display:none">
|
| 412 |
-
<p class="file-name hidden" id="file-name"></p>
|
| 413 |
-
</div>
|
| 414 |
-
<div class="upload-actions">
|
| 415 |
-
<button class="btn-primary" id="load-btn" disabled>
|
| 416 |
-
<span id="load-btn-text">Load PDF</span>
|
| 417 |
-
<span class="spinner hidden" id="load-spinner"></span>
|
| 418 |
-
</button>
|
| 419 |
-
</div>
|
| 420 |
-
<div class="status hidden" id="status-msg"></div>
|
| 421 |
-
</div>
|
| 422 |
-
|
| 423 |
<!-- ββ Quiz card βββββοΏ½οΏ½ββββββββββββββββββββββββββββββββββββββββββββββββ -->
|
| 424 |
<div class="card hidden fade-in-up" id="quiz-card" style="animation-delay:.1s">
|
| 425 |
|
|
@@ -546,9 +471,8 @@
|
|
| 546 |
total: 0,
|
| 547 |
language: 'English',
|
| 548 |
difficulty: 'Normal',
|
| 549 |
-
history: [],
|
| 550 |
loaded: false,
|
| 551 |
-
selectedFile: null,
|
| 552 |
};
|
| 553 |
|
| 554 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -556,16 +480,6 @@
|
|
| 556 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 557 |
const $ = id => document.getElementById(id);
|
| 558 |
|
| 559 |
-
const dropZone = $('drop-zone');
|
| 560 |
-
const fileInput = $('file-input');
|
| 561 |
-
const fileNameEl = $('file-name');
|
| 562 |
-
const dropTitle = $('drop-title');
|
| 563 |
-
const uploadIcon = $('upload-icon');
|
| 564 |
-
const loadBtn = $('load-btn');
|
| 565 |
-
const loadBtnText = $('load-btn-text');
|
| 566 |
-
const loadSpinner = $('load-spinner');
|
| 567 |
-
const statusEl = $('status-msg');
|
| 568 |
-
|
| 569 |
const quizCard = $('quiz-card');
|
| 570 |
const emptyState = $('empty-state');
|
| 571 |
const questionArea = $('question-area');
|
|
@@ -598,15 +512,6 @@
|
|
| 598 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 599 |
// API helpers
|
| 600 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 601 |
-
async function apiLoad(file) {
|
| 602 |
-
const fd = new FormData();
|
| 603 |
-
fd.append('file', file);
|
| 604 |
-
const r = await fetch('/api/load', { method: 'POST', body: fd });
|
| 605 |
-
const d = await r.json();
|
| 606 |
-
if (!r.ok) throw new Error(d.detail || 'Failed to load PDF');
|
| 607 |
-
return d;
|
| 608 |
-
}
|
| 609 |
-
|
| 610 |
async function apiQuestion(chunk, language, difficulty) {
|
| 611 |
const r = await fetch('/api/question', {
|
| 612 |
method: 'POST',
|
|
@@ -632,13 +537,6 @@
|
|
| 632 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 633 |
// UI helpers
|
| 634 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 635 |
-
function setStatus(msg, type = 'info') {
|
| 636 |
-
statusEl.textContent = msg;
|
| 637 |
-
statusEl.className = `status ${type}`;
|
| 638 |
-
statusEl.classList.remove('hidden');
|
| 639 |
-
}
|
| 640 |
-
function clearStatus() { statusEl.classList.add('hidden'); }
|
| 641 |
-
|
| 642 |
function setLoadingBtn(btn, textEl, spinner, loading, label) {
|
| 643 |
btn.disabled = loading;
|
| 644 |
textEl.textContent = label || textEl.textContent;
|
|
@@ -651,7 +549,6 @@
|
|
| 651 |
const pct = d === 0 ? 0 : n / d;
|
| 652 |
const circ = 138.2;
|
| 653 |
scoreArc.style.strokeDashoffset = circ * (1 - pct);
|
| 654 |
-
// colour: green when >70%, amber when >40%, red otherwise
|
| 655 |
const color = pct >= 0.7 ? '#10B981' : pct >= 0.4 ? '#F59E0B' : '#EF4444';
|
| 656 |
scoreArc.style.stroke = d === 0
|
| 657 |
? 'url(#scoreGrad)'
|
|
@@ -668,7 +565,6 @@
|
|
| 668 |
if (l.includes('correct')) return 'correct';
|
| 669 |
}
|
| 670 |
}
|
| 671 |
-
// fallback: scan first 200 chars
|
| 672 |
if (lower.slice(0,200).includes('incorrect')) return 'incorrect';
|
| 673 |
if (lower.slice(0,200).includes('partially') || lower.slice(0,200).includes('partiellement')) return 'partial';
|
| 674 |
if (lower.slice(0,200).includes('correct')) return 'correct';
|
|
@@ -676,8 +572,6 @@
|
|
| 676 |
}
|
| 677 |
|
| 678 |
function renderFeedback(text) {
|
| 679 |
-
// Try to split into numbered sections, fallback to raw text
|
| 680 |
-
const sectionRe = /^(\d+)\.\s*(.*?)(?=\n\d+\.|$)/gms;
|
| 681 |
const sectionLabels = ['', 'Verdict', 'What was good', 'What was missing', 'Model answer'];
|
| 682 |
const frLabels = ['', 'Verdict', 'Ce qui était bien', 'Ce qui manquait', 'Réponse modèle'];
|
| 683 |
const labels = S.language === 'FranΓ§ais' ? frLabels : sectionLabels;
|
|
@@ -697,7 +591,6 @@
|
|
| 697 |
</div>`;
|
| 698 |
}).join('');
|
| 699 |
} else {
|
| 700 |
-
// Plain fallback
|
| 701 |
feedbackBody.innerHTML = `<div class="feedback-raw">${
|
| 702 |
text.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
| 703 |
}</div>`;
|
|
@@ -708,9 +601,9 @@
|
|
| 708 |
const verdict = extractVerdict(text);
|
| 709 |
verdictBadge.className = `verdict-badge ${verdict === 'unknown' ? '' : verdict}`;
|
| 710 |
const labels = {
|
| 711 |
-
correct: S.language === 'FranΓ§ais' ? 'β Correct'
|
| 712 |
partial: S.language === 'FranΓ§ais' ? '~ Partiellement correct' : '~ Partially Correct',
|
| 713 |
-
incorrect: S.language === 'FranΓ§ais' ? 'β Incorrect'
|
| 714 |
unknown: 'β Unknown',
|
| 715 |
};
|
| 716 |
verdictBadge.textContent = labels[verdict] || labels.unknown;
|
|
@@ -737,7 +630,6 @@
|
|
| 737 |
: ["Keep going β each question makes you stronger. π₯", "Continue β chaque question te rend plus fort. π₯"];
|
| 738 |
modalMessage.textContent = S.language === 'FranΓ§ais' ? msgs[1] : msgs[0];
|
| 739 |
|
| 740 |
-
// History list (last 8 items)
|
| 741 |
const recent = S.history.slice(-8);
|
| 742 |
if (recent.length > 0) {
|
| 743 |
modalHistory.classList.remove('hidden');
|
|
@@ -756,63 +648,6 @@
|
|
| 756 |
modal.classList.remove('hidden');
|
| 757 |
}
|
| 758 |
|
| 759 |
-
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 760 |
-
// File handling
|
| 761 |
-
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 762 |
-
function handleFile(file) {
|
| 763 |
-
if (!file || !file.name.toLowerCase().endsWith('.pdf')) {
|
| 764 |
-
setStatus('Please select a PDF file.', 'error');
|
| 765 |
-
return;
|
| 766 |
-
}
|
| 767 |
-
S.selectedFile = file;
|
| 768 |
-
fileNameEl.textContent = 'π ' + file.name;
|
| 769 |
-
fileNameEl.classList.remove('hidden');
|
| 770 |
-
dropTitle.textContent = 'PDF selected';
|
| 771 |
-
uploadIcon.textContent = 'β
';
|
| 772 |
-
dropZone.classList.add('has-file');
|
| 773 |
-
loadBtn.disabled = false;
|
| 774 |
-
clearStatus();
|
| 775 |
-
}
|
| 776 |
-
|
| 777 |
-
dropZone.addEventListener('dragover', e => {
|
| 778 |
-
e.preventDefault();
|
| 779 |
-
dropZone.classList.add('drag-over');
|
| 780 |
-
});
|
| 781 |
-
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
|
| 782 |
-
dropZone.addEventListener('drop', e => {
|
| 783 |
-
e.preventDefault();
|
| 784 |
-
dropZone.classList.remove('drag-over');
|
| 785 |
-
const file = e.dataTransfer.files[0];
|
| 786 |
-
if (file) { fileInput.files = e.dataTransfer.files; handleFile(file); }
|
| 787 |
-
});
|
| 788 |
-
dropZone.addEventListener('click', () => fileInput.click());
|
| 789 |
-
fileInput.addEventListener('change', () => {
|
| 790 |
-
if (fileInput.files[0]) handleFile(fileInput.files[0]);
|
| 791 |
-
});
|
| 792 |
-
|
| 793 |
-
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 794 |
-
// Load PDF
|
| 795 |
-
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 796 |
-
loadBtn.addEventListener('click', async () => {
|
| 797 |
-
if (!S.selectedFile) return;
|
| 798 |
-
setLoadingBtn(loadBtn, loadBtnText, loadSpinner, true, 'Loadingβ¦');
|
| 799 |
-
setStatus('Parsing PDFβ¦', 'info');
|
| 800 |
-
try {
|
| 801 |
-
const data = await apiLoad(S.selectedFile);
|
| 802 |
-
S.chunks = data.chunks;
|
| 803 |
-
S.loaded = true;
|
| 804 |
-
S.correct = 0; S.total = 0; S.history = [];
|
| 805 |
-
updateScore();
|
| 806 |
-
setStatus(`β ${data.count} sections extracted β ready to quiz!`, 'ok');
|
| 807 |
-
quizCard.classList.remove('hidden');
|
| 808 |
-
quizCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
| 809 |
-
} catch (err) {
|
| 810 |
-
setStatus(err.message, 'error');
|
| 811 |
-
} finally {
|
| 812 |
-
setLoadingBtn(loadBtn, loadBtnText, loadSpinner, false, 'Load PDF');
|
| 813 |
-
}
|
| 814 |
-
});
|
| 815 |
-
|
| 816 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 817 |
// Language / difficulty toggles
|
| 818 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -835,14 +670,12 @@
|
|
| 835 |
async function fetchQuestion() {
|
| 836 |
if (!S.chunks.length) return;
|
| 837 |
|
| 838 |
-
// Switch from empty state to question area on first use
|
| 839 |
if (emptyState && !emptyState.classList.contains('hidden')) {
|
| 840 |
emptyState.classList.add('hidden');
|
| 841 |
questionArea.classList.remove('hidden');
|
| 842 |
endBtn.classList.remove('hidden');
|
| 843 |
}
|
| 844 |
|
| 845 |
-
// Show loading inside question box
|
| 846 |
qText.textContent = '';
|
| 847 |
qLoading.classList.remove('hidden');
|
| 848 |
qLoadingMsg.textContent = S.total === 0
|
|
@@ -924,7 +757,6 @@
|
|
| 924 |
|
| 925 |
modalClose.addEventListener('click', () => {
|
| 926 |
modal.classList.add('hidden');
|
| 927 |
-
// Reset session
|
| 928 |
S.correct = 0; S.total = 0; S.history = [];
|
| 929 |
S.currentQuestion = ''; S.currentChunk = '';
|
| 930 |
updateScore();
|
|
@@ -982,7 +814,6 @@
|
|
| 982 |
if (p.y < -5) p.y = H + 5;
|
| 983 |
if (p.y > H + 5) p.y = -5;
|
| 984 |
|
| 985 |
-
// Connections
|
| 986 |
for (let j = i + 1; j < particles.length; j++) {
|
| 987 |
const q = particles[j];
|
| 988 |
const dx = p.x - q.x, dy = p.y - q.y;
|
|
@@ -997,7 +828,6 @@
|
|
| 997 |
}
|
| 998 |
}
|
| 999 |
|
| 1000 |
-
// Dot
|
| 1001 |
ctx.beginPath();
|
| 1002 |
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
|
| 1003 |
const hex = p.hue === '#7C3AED' ? '124,58,237' : '6,182,212';
|
|
|
|
| 94 |
}
|
| 95 |
.card:hover { border-color: rgba(255,255,255,.12); }
|
| 96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
/* ββ Buttons ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 98 |
button { cursor: pointer; font-family: inherit; font-size: .95rem;
|
| 99 |
border: none; outline: none; transition: all .2s; }
|
|
|
|
| 124 |
}
|
| 125 |
.btn-ghost:disabled { opacity: .35; cursor: not-allowed; }
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
/* ββ Pill toggles βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 128 |
.toggle-group { display: flex; gap: 6px; }
|
| 129 |
|
|
|
|
| 139 |
box-shadow: 0 2px 12px rgba(124,58,237,.4);
|
| 140 |
}
|
| 141 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
/* ββ Quiz controls row βββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 143 |
.controls-row {
|
| 144 |
display: flex; align-items: center; flex-wrap: wrap; gap: 12px;
|
|
|
|
| 345 |
<p class="hero-sub">Upload your course. Get quizzed by AI.</p>
|
| 346 |
</header>
|
| 347 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
<!-- ββ Quiz card βββββοΏ½οΏ½ββββββββββββββββββββββββββββββββββββββββββββββββ -->
|
| 349 |
<div class="card hidden fade-in-up" id="quiz-card" style="animation-delay:.1s">
|
| 350 |
|
|
|
|
| 471 |
total: 0,
|
| 472 |
language: 'English',
|
| 473 |
difficulty: 'Normal',
|
| 474 |
+
history: [],
|
| 475 |
loaded: false,
|
|
|
|
| 476 |
};
|
| 477 |
|
| 478 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 480 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 481 |
const $ = id => document.getElementById(id);
|
| 482 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
const quizCard = $('quiz-card');
|
| 484 |
const emptyState = $('empty-state');
|
| 485 |
const questionArea = $('question-area');
|
|
|
|
| 512 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 513 |
// API helpers
|
| 514 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 515 |
async function apiQuestion(chunk, language, difficulty) {
|
| 516 |
const r = await fetch('/api/question', {
|
| 517 |
method: 'POST',
|
|
|
|
| 537 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 538 |
// UI helpers
|
| 539 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
function setLoadingBtn(btn, textEl, spinner, loading, label) {
|
| 541 |
btn.disabled = loading;
|
| 542 |
textEl.textContent = label || textEl.textContent;
|
|
|
|
| 549 |
const pct = d === 0 ? 0 : n / d;
|
| 550 |
const circ = 138.2;
|
| 551 |
scoreArc.style.strokeDashoffset = circ * (1 - pct);
|
|
|
|
| 552 |
const color = pct >= 0.7 ? '#10B981' : pct >= 0.4 ? '#F59E0B' : '#EF4444';
|
| 553 |
scoreArc.style.stroke = d === 0
|
| 554 |
? 'url(#scoreGrad)'
|
|
|
|
| 565 |
if (l.includes('correct')) return 'correct';
|
| 566 |
}
|
| 567 |
}
|
|
|
|
| 568 |
if (lower.slice(0,200).includes('incorrect')) return 'incorrect';
|
| 569 |
if (lower.slice(0,200).includes('partially') || lower.slice(0,200).includes('partiellement')) return 'partial';
|
| 570 |
if (lower.slice(0,200).includes('correct')) return 'correct';
|
|
|
|
| 572 |
}
|
| 573 |
|
| 574 |
function renderFeedback(text) {
|
|
|
|
|
|
|
| 575 |
const sectionLabels = ['', 'Verdict', 'What was good', 'What was missing', 'Model answer'];
|
| 576 |
const frLabels = ['', 'Verdict', 'Ce qui était bien', 'Ce qui manquait', 'Réponse modèle'];
|
| 577 |
const labels = S.language === 'FranΓ§ais' ? frLabels : sectionLabels;
|
|
|
|
| 591 |
</div>`;
|
| 592 |
}).join('');
|
| 593 |
} else {
|
|
|
|
| 594 |
feedbackBody.innerHTML = `<div class="feedback-raw">${
|
| 595 |
text.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
| 596 |
}</div>`;
|
|
|
|
| 601 |
const verdict = extractVerdict(text);
|
| 602 |
verdictBadge.className = `verdict-badge ${verdict === 'unknown' ? '' : verdict}`;
|
| 603 |
const labels = {
|
| 604 |
+
correct: S.language === 'FranΓ§ais' ? 'β Correct' : 'β Correct',
|
| 605 |
partial: S.language === 'FranΓ§ais' ? '~ Partiellement correct' : '~ Partially Correct',
|
| 606 |
+
incorrect: S.language === 'FranΓ§ais' ? 'β Incorrect' : 'β Incorrect',
|
| 607 |
unknown: 'β Unknown',
|
| 608 |
};
|
| 609 |
verdictBadge.textContent = labels[verdict] || labels.unknown;
|
|
|
|
| 630 |
: ["Keep going β each question makes you stronger. π₯", "Continue β chaque question te rend plus fort. π₯"];
|
| 631 |
modalMessage.textContent = S.language === 'FranΓ§ais' ? msgs[1] : msgs[0];
|
| 632 |
|
|
|
|
| 633 |
const recent = S.history.slice(-8);
|
| 634 |
if (recent.length > 0) {
|
| 635 |
modalHistory.classList.remove('hidden');
|
|
|
|
| 648 |
modal.classList.remove('hidden');
|
| 649 |
}
|
| 650 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 651 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 652 |
// Language / difficulty toggles
|
| 653 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 670 |
async function fetchQuestion() {
|
| 671 |
if (!S.chunks.length) return;
|
| 672 |
|
|
|
|
| 673 |
if (emptyState && !emptyState.classList.contains('hidden')) {
|
| 674 |
emptyState.classList.add('hidden');
|
| 675 |
questionArea.classList.remove('hidden');
|
| 676 |
endBtn.classList.remove('hidden');
|
| 677 |
}
|
| 678 |
|
|
|
|
| 679 |
qText.textContent = '';
|
| 680 |
qLoading.classList.remove('hidden');
|
| 681 |
qLoadingMsg.textContent = S.total === 0
|
|
|
|
| 757 |
|
| 758 |
modalClose.addEventListener('click', () => {
|
| 759 |
modal.classList.add('hidden');
|
|
|
|
| 760 |
S.correct = 0; S.total = 0; S.history = [];
|
| 761 |
S.currentQuestion = ''; S.currentChunk = '';
|
| 762 |
updateScore();
|
|
|
|
| 814 |
if (p.y < -5) p.y = H + 5;
|
| 815 |
if (p.y > H + 5) p.y = -5;
|
| 816 |
|
|
|
|
| 817 |
for (let j = i + 1; j < particles.length; j++) {
|
| 818 |
const q = particles[j];
|
| 819 |
const dx = p.x - q.x, dy = p.y - q.y;
|
|
|
|
| 828 |
}
|
| 829 |
}
|
| 830 |
|
|
|
|
| 831 |
ctx.beginPath();
|
| 832 |
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
|
| 833 |
const hex = p.hue === '#7C3AED' ? '124,58,237' : '6,182,212';
|