smartsaduvu / app /examprep.html
pavan1221's picture
Update app/examprep.html
1646192 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>Exam Prep AI</title>
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg:#f0f5fb; --bg2:#e4edf8; --surface:#ffffff; --surface2:#f7faff;
--border:#d0ddef; --border2:#b8cce4; --blue:#2563eb; --blue-mid:#3b82f6;
--blue-light:#eff6ff; --blue-pale:#dbeafe; --text:#0f172a; --text2:#334155;
--muted:#64748b; --green:#16a34a; --green-bg:#f0fdf4; --red:#dc2626;
--red-bg:#fff1f2; --amber:#d97706; --amber-bg:#fffbeb;
--shadow:0 1px 6px rgba(37,99,235,0.07),0 2px 4px rgba(15,23,42,0.04);
--shadow-md:0 4px 20px rgba(37,99,235,0.10),0 2px 8px rgba(15,23,42,0.05);
--shadow-lg:0 12px 40px rgba(37,99,235,0.13);
--radius:16px; --touch-min:44px;
}
html { -webkit-text-size-adjust:100%; scroll-behavior:smooth; }
body {
background:var(--bg); color:var(--text); font-family:'DM Sans',sans-serif;
min-height:100dvh; display:flex; justify-content:center;
padding:1.5rem 1rem 6rem;
background-image:radial-gradient(ellipse at 20% 0%,rgba(37,99,235,0.08) 0%,transparent 60%),
radial-gradient(ellipse at 80% 100%,rgba(59,130,246,0.06) 0%,transparent 60%);
-webkit-tap-highlight-color:transparent;
}
.wrap { width:100%; max-width:700px; }
.screen { display:none; }
.screen.active { display:block; }
@keyframes slideInRight { from{opacity:0;transform:translateX(40px);} to{opacity:1;transform:translateX(0);} }
@keyframes slideInLeft { from{opacity:0;transform:translateX(-40px);} to{opacity:1;transform:translateX(0);} }
@keyframes slideOutLeft { from{opacity:1;transform:translateX(0);} to{opacity:0;transform:translateX(-40px);} }
@keyframes slideOutRight { from{opacity:1;transform:translateX(0);} to{opacity:0;transform:translateX(40px);} }
@keyframes fadeUp { from{opacity:0;transform:translateY(12px);} to{opacity:1;transform:translateY(0);} }
@keyframes spinning { to{transform:rotate(360deg);} }
@keyframes throb { from{opacity:1;} to{opacity:0.45;} }
@keyframes pulse-orb { 0%,100%{transform:scale(1);} 50%{transform:scale(1.07);} }
@keyframes confetti-fall { 0%{transform:translateY(-20px) rotate(0deg);opacity:1;} 100%{transform:translateY(100vh) rotate(720deg);opacity:0;} }
@keyframes slideDown { from{opacity:0;transform:translateY(-8px);} to{opacity:1;transform:translateY(0);} }
/* BRAND */
.brand { text-align:center; padding:1.5rem 0 2rem; animation:fadeUp 0.4s ease; }
.brand-pill { display:inline-block; background:var(--blue-pale); color:var(--blue); border:1px solid rgba(37,99,235,0.2); border-radius:30px; padding:0.3rem 1rem; font-size:0.68rem; font-weight:700; letter-spacing:0.12em; text-transform:uppercase; margin-bottom:0.9rem; }
.brand-title { font-family:'DM Serif Display',serif; font-size:clamp(1.75rem,6vw,3rem); font-weight:400; line-height:1.1; color:var(--text); margin-bottom:0.45rem; }
.brand-title span { color:var(--blue); font-style:italic; }
.brand-sub { color:var(--muted); font-size:clamp(0.8rem,2.5vw,0.92rem); }
/* CARD */
.card { background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); padding:clamp(1.2rem,4vw,1.8rem); box-shadow:var(--shadow-md); margin-bottom:0.8rem; }
.card-label { font-size:0.62rem; text-transform:uppercase; letter-spacing:0.12em; color:var(--muted); font-weight:700; margin-bottom:0.85rem; }
/* DROP ZONE */
.drop-zone { border:2px dashed var(--border2); border-radius:12px; padding:clamp(1.6rem,5vw,2.2rem) 1rem; text-align:center; cursor:pointer; transition:all 0.2s; position:relative; background:var(--bg); }
.drop-zone:hover,.drop-zone.over { border-color:var(--blue); background:var(--blue-light); }
.drop-zone input { position:absolute; inset:0; opacity:0; cursor:pointer; width:100%; height:100%; }
.drop-icon-svg { width:36px; height:36px; margin:0 auto 0.6rem; color:var(--blue-mid); display:block; }
.drop-title { font-size:clamp(0.88rem,2.8vw,1rem); font-weight:700; margin-bottom:0.2rem; }
.drop-sub { font-size:0.73rem; color:var(--muted); margin-bottom:0.6rem; }
.drop-badge { display:inline-flex; align-items:center; gap:0.3rem; background:var(--blue-pale); color:var(--blue); border:1px solid rgba(37,99,235,0.2); border-radius:20px; padding:0.22rem 0.75rem; font-size:0.67rem; font-weight:700; }
.pdf-list { display:flex; flex-direction:column; gap:0.45rem; margin-bottom:0.6rem; }
.pdf-item { display:flex; align-items:center; gap:0.65rem; background:var(--surface); border:1.5px solid var(--border); border-radius:12px; padding:0.7rem 0.9rem; animation:slideDown 0.22s ease; box-shadow:var(--shadow); }
.pdf-icon-wrap { width:36px; height:36px; border-radius:8px; background:var(--blue-light); border:1px solid var(--blue-pale); display:flex; align-items:center; justify-content:center; flex-shrink:0; }
.pdf-icon-wrap svg { width:18px; height:18px; color:var(--blue); }
.pdf-info { flex:1; min-width:0; }
.pdf-name { font-size:0.8rem; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.pdf-meta { font-size:0.67rem; color:var(--muted); margin-top:0.1rem; }
.pdf-status { font-size:0.62rem; font-weight:700; padding:0.18rem 0.55rem; border-radius:20px; white-space:nowrap; }
.pdf-status.ok { background:var(--green-bg); color:var(--green); border:1px solid rgba(22,163,74,0.2); }
.pdf-remove { width:30px; height:30px; border-radius:8px; border:1.5px solid var(--border); background:var(--surface2); color:var(--muted); cursor:pointer; display:flex; align-items:center; justify-content:center; transition:all 0.15s; flex-shrink:0; }
.pdf-remove:hover { background:var(--red-bg); border-color:rgba(220,38,38,0.3); color:var(--red); }
.pdf-remove svg { width:14px; height:14px; }
.pdf-add-row { display:flex; gap:0.5rem; align-items:stretch; }
.add-drop-zone { flex:1; border:2px dashed var(--border2); border-radius:10px; padding:0.7rem 1rem; text-align:center; cursor:pointer; transition:all 0.2s; position:relative; background:var(--bg); display:flex; align-items:center; justify-content:center; gap:0.5rem; font-size:0.78rem; font-weight:600; color:var(--muted); min-height:var(--touch-min); }
.add-drop-zone:hover,.add-drop-zone.over { border-color:var(--blue); color:var(--blue); background:var(--blue-light); }
.add-drop-zone input { position:absolute; inset:0; opacity:0; cursor:pointer; width:100%; height:100%; }
.add-drop-zone svg { width:16px; height:16px; flex-shrink:0; }
.pdf-count-badge { display:flex; align-items:center; justify-content:center; background:var(--bg2); border:1.5px solid var(--border); border-radius:10px; padding:0.7rem 0.85rem; font-size:0.72rem; font-weight:700; color:var(--muted); white-space:nowrap; }
.pdf-count-badge span { color:var(--blue); font-size:1rem; font-family:'DM Serif Display',serif; margin-right:0.25rem; }
.scan-warn { background:var(--amber-bg); border:1.5px solid rgba(217,119,6,0.3); border-radius:10px; padding:0.7rem 1rem; font-size:0.78rem; color:var(--amber); font-weight:600; margin-bottom:0.8rem; display:none; line-height:1.5; }
.scan-warn.show { display:block; }
/* SETTINGS */
.settings-grid { display:grid; grid-template-columns:1fr 1fr; gap:0.7rem; margin-bottom:0.9rem; }
.field { display:flex; flex-direction:column; gap:0.28rem; }
.field-label { font-size:0.61rem; text-transform:uppercase; letter-spacing:0.1em; color:var(--muted); font-weight:700; }
select, input[type="number"], input[type="text"] { background:var(--surface2); border:1.5px solid var(--border); border-radius:10px; color:var(--text); padding:0.65rem 0.85rem; font-family:'DM Sans',sans-serif; font-size:clamp(0.82rem,2.5vw,0.88rem); outline:none; transition:border 0.2s,box-shadow 0.2s; width:100%; appearance:none; min-height:var(--touch-min); }
select:focus,input:focus { border-color:var(--blue); box-shadow:0 0 0 3px rgba(37,99,235,0.1); }
/* TIMER */
.timer-section-wrap { margin-bottom:0.9rem; }
.timer-mode-tabs { display:flex; background:var(--bg2); border:1.5px solid var(--border); border-radius:10px; overflow:hidden; margin-bottom:0.75rem; }
.tmt-btn { flex:1; padding:0.65rem 0.3rem; border:none; background:transparent; font-family:'DM Sans',sans-serif; font-size:clamp(0.71rem,2.2vw,0.8rem); font-weight:600; color:var(--muted); cursor:pointer; transition:all 0.18s; min-height:var(--touch-min); }
.tmt-btn.active { background:var(--blue); color:white; }
.timer-panel { display:none; animation:fadeUp 0.22s ease; }
.timer-panel.show { display:block; }
.time-presets { display:flex; gap:0.45rem; flex-wrap:wrap; }
.time-btn { padding:0.42rem 0.85rem; border-radius:20px; border:1.5px solid var(--border); background:var(--surface2); color:var(--text2); font-family:'DM Sans',sans-serif; font-size:clamp(0.72rem,2.2vw,0.8rem); font-weight:600; cursor:pointer; transition:all 0.18s; min-height:var(--touch-min); display:inline-flex; align-items:center; }
.time-btn:hover { border-color:var(--blue); color:var(--blue); }
.time-btn.sel { background:var(--blue); border-color:var(--blue); color:white; }
.custom-row { display:none; margin-top:0.6rem; gap:0.5rem; align-items:center; }
.custom-row.show { display:flex; }
.custom-row input { max-width:130px; }
.custom-unit { font-size:0.78rem; color:var(--muted); font-weight:600; }
/* START BTN */
.btn-start { width:100%; padding:1rem; background:linear-gradient(135deg,var(--blue),var(--blue-mid)); color:white; border:none; border-radius:12px; font-family:'DM Sans',sans-serif; font-size:clamp(0.9rem,2.8vw,1rem); font-weight:700; cursor:pointer; transition:all 0.22s; display:flex; align-items:center; justify-content:center; gap:0.6rem; box-shadow:0 4px 18px rgba(37,99,235,0.35); min-height:var(--touch-min); }
.btn-start:hover:not(:disabled) { transform:translateY(-2px); box-shadow:0 8px 28px rgba(37,99,235,0.42); }
.btn-start:disabled { opacity:0.4; cursor:not-allowed; transform:none; }
.spin { width:17px; height:17px; border:2.5px solid rgba(255,255,255,0.35); border-top-color:white; border-radius:50%; animation:spinning 0.7s linear infinite; display:none; }
.err-toast { background:var(--red-bg); border:1px solid rgba(220,38,38,0.2); border-radius:10px; padding:0.7rem 1rem; color:var(--red); font-size:0.79rem; display:none; margin-top:0.8rem; line-height:1.5; }
/* LOADING */
.loading-overlay { display:none; position:fixed; inset:0; background:rgba(240,245,251,0.96); backdrop-filter:blur(12px); z-index:200; flex-direction:column; align-items:center; justify-content:center; text-align:center; padding:2rem; }
.loading-overlay.show { display:flex; }
.loading-orb { width:68px; height:68px; border-radius:50%; background:linear-gradient(135deg,var(--blue),var(--blue-mid)); display:flex; align-items:center; justify-content:center; margin-bottom:1.3rem; animation:pulse-orb 1.6s ease infinite; box-shadow:0 8px 30px rgba(37,99,235,0.35); }
.loading-orb svg { width:28px; height:28px; color:white; }
.loading-title { font-family:'DM Serif Display',serif; font-size:clamp(1.2rem,4vw,1.5rem); margin-bottom:0.35rem; }
.loading-sub { color:var(--muted); font-size:0.83rem; margin-bottom:1rem; }
.loading-sources { display:flex; gap:0.35rem; flex-wrap:wrap; justify-content:center; margin-bottom:1rem; max-width:320px; }
.ls-badge { background:var(--blue-pale); color:var(--blue); border-radius:20px; padding:0.2rem 0.7rem; font-size:0.66rem; font-weight:700; border:1px solid rgba(37,99,235,0.2); }
.real-progress-wrap { width:min(280px,75vw); margin-bottom:0.6rem; }
.real-progress-track { height:8px; background:var(--border); border-radius:4px; overflow:hidden; }
.real-progress-fill { height:100%; background:linear-gradient(90deg,var(--blue),var(--blue-mid)); border-radius:4px; transition:width 0.4s ease; width:0%; }
.real-progress-pct { font-size:0.72rem; color:var(--blue); font-weight:700; margin-top:0.3rem; }
.loading-msg { font-size:0.78rem; color:var(--muted); margin-top:0.4rem; min-height:1.2em; }
.scan-overlay-warn { background:var(--amber-bg); border:1.5px solid rgba(217,119,6,0.3); border-radius:10px; padding:0.6rem 1rem; font-size:0.74rem; color:var(--amber); font-weight:600; margin-top:0.8rem; display:none; max-width:320px; line-height:1.5; }
.scan-overlay-warn.show { display:block; }
/* QUIZ TOP */
.quiz-top { display:flex; align-items:center; gap:0.6rem; margin-bottom:0.9rem; animation:fadeUp 0.3s ease; flex-wrap:wrap; }
.score-chip { background:var(--surface); border:1.5px solid var(--border); border-radius:20px; padding:0.28rem 0.85rem; font-size:clamp(0.7rem,2vw,0.78rem); font-weight:700; white-space:nowrap; box-shadow:var(--shadow); color:var(--text2); }
.prog-wrap { flex:1; min-width:100px; }
.prog-bar { height:5px; background:var(--border); border-radius:3px; overflow:hidden; }
.prog-fill { height:100%; background:linear-gradient(90deg,var(--blue),var(--blue-mid)); border-radius:3px; transition:width 0.45s cubic-bezier(0.4,0,0.2,1); }
.prog-text { font-size:0.65rem; color:var(--muted); margin-top:0.28rem; font-weight:600; }
.total-timer { background:var(--blue-light); border:1.5px solid var(--blue-pale); border-radius:20px; padding:0.28rem 0.85rem; font-size:clamp(0.7rem,2vw,0.78rem); font-weight:700; color:var(--blue); white-space:nowrap; display:none; font-variant-numeric:tabular-nums; }
.total-timer.warn { background:var(--amber-bg); border-color:rgba(217,119,6,0.3); color:var(--amber); }
.total-timer.hot { background:var(--red-bg); border-color:rgba(220,38,38,0.3); color:var(--red); animation:throb 0.5s ease infinite alternate; }
.perq-timer { background:var(--amber-bg); border:1.5px solid rgba(217,119,6,0.3); border-radius:20px; padding:0.28rem 0.85rem; font-size:clamp(0.7rem,2vw,0.78rem); font-weight:700; color:var(--amber); white-space:nowrap; display:none; font-variant-numeric:tabular-nums; }
.perq-timer.hot { background:var(--red-bg); border-color:rgba(220,38,38,0.3); color:var(--red); animation:throb 0.5s ease infinite alternate; }
/* QUESTION CARD */
.question-viewport { position:relative; overflow:hidden; border-radius:var(--radius); margin-bottom:0.8rem; }
.question-card { background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); padding:clamp(1.1rem,4vw,1.8rem); box-shadow:var(--shadow-lg); min-height:clamp(280px,50vw,380px); display:flex; flex-direction:column; }
.q-slide-in-right { animation:slideInRight 0.38s cubic-bezier(0.4,0,0.2,1) forwards; }
.q-slide-in-left { animation:slideInLeft 0.38s cubic-bezier(0.4,0,0.2,1) forwards; }
.q-slide-out-left { animation:slideOutLeft 0.32s cubic-bezier(0.4,0,0.2,1) forwards; }
.q-slide-out-right { animation:slideOutRight 0.32s cubic-bezier(0.4,0,0.2,1) forwards; }
.q-meta { display:flex; gap:0.38rem; margin-bottom:0.9rem; align-items:center; flex-wrap:wrap; }
.tag { font-size:0.57rem; text-transform:uppercase; letter-spacing:0.1em; padding:0.18rem 0.55rem; border-radius:20px; border:1.5px solid var(--border); color:var(--muted); font-weight:700; }
.tag.easy { border-color:rgba(22,163,74,0.3); color:var(--green); background:var(--green-bg); }
.tag.medium { border-color:rgba(217,119,6,0.3); color:var(--amber); background:var(--amber-bg); }
.tag.hard { border-color:rgba(220,38,38,0.3); color:var(--red); background:var(--red-bg); }
.tag.bluetag { border-color:rgba(37,99,235,0.25); color:var(--blue); background:var(--blue-light); }
.q-num { font-size:0.65rem; color:var(--muted); font-weight:700; margin-left:auto; }
.q-text { font-family:'DM Serif Display',serif; font-size:clamp(0.95rem,3vw,1.15rem); line-height:1.65; color:var(--text); margin-bottom:1.1rem; font-weight:400; flex:1; }
.multi-notice { background:var(--blue-light); border:1px solid rgba(37,99,235,0.18); border-radius:8px; padding:0.38rem 0.75rem; font-size:0.69rem; color:var(--blue); font-weight:600; margin-bottom:0.65rem; display:none; }
.opts { display:flex; flex-direction:column; gap:0.45rem; }
.opt { width:100%; background:var(--surface2); border:1.5px solid var(--border); border-radius:10px; padding:0.75rem 1rem; color:var(--text); font-family:'DM Sans',sans-serif; font-size:clamp(0.82rem,2.5vw,0.88rem); cursor:pointer; transition:all 0.15s; text-align:left; display:flex; align-items:center; gap:0.7rem; font-weight:500; min-height:var(--touch-min); }
.opt:hover:not(:disabled) { border-color:var(--blue-mid); background:var(--blue-light); }
.opt.sel { border-color:var(--blue); background:var(--blue-pale); }
.opt-letter { width:28px; height:28px; border-radius:7px; border:1.5px solid var(--border2); display:flex; align-items:center; justify-content:center; font-size:0.7rem; font-weight:800; flex-shrink:0; background:var(--surface); transition:all 0.15s; color:var(--muted); }
.opt.sel .opt-letter { background:var(--blue); border-color:var(--blue); color:white; }
.ans-input, .ans-ta { width:100%; background:var(--surface2); border:1.5px solid var(--border); border-radius:10px; color:var(--text); padding:0.82rem 1rem; font-family:'DM Sans',sans-serif; font-size:clamp(0.84rem,2.5vw,0.9rem); outline:none; transition:border 0.2s,box-shadow 0.2s; min-height:var(--touch-min); }
.ans-input:focus,.ans-ta:focus { border-color:var(--blue); box-shadow:0 0 0 3px rgba(37,99,235,0.1); }
.ans-ta { resize:vertical; min-height:clamp(90px,20vw,120px); }
.hint-box { background:var(--amber-bg); border:1.5px solid rgba(217,119,6,0.22); border-radius:10px; padding:0.7rem 1rem; font-size:0.78rem; color:var(--amber); margin-top:0.75rem; display:none; line-height:1.55; font-weight:500; }
.skipped-badge { background:var(--bg2); border:1.5px solid var(--border2); border-radius:8px; padding:0.5rem 0.8rem; font-size:0.76rem; color:var(--muted); font-weight:600; margin-top:0.5rem; display:none; text-align:center; }
/* ACTIONS — updated grid with Next button */
.actions-grid { display:grid; grid-template-columns:1fr 1fr 1fr; grid-template-rows:auto auto auto; gap:0.5rem; margin-top:0.7rem; animation:fadeUp 0.3s ease; }
.btn { padding:0.75rem 0.5rem; border-radius:10px; border:1.5px solid var(--border); background:var(--surface); color:var(--muted); font-family:'DM Sans',sans-serif; font-size:clamp(0.75rem,2.2vw,0.82rem); cursor:pointer; transition:all 0.15s; font-weight:600; box-shadow:var(--shadow); text-align:center; min-height:var(--touch-min); display:flex; align-items:center; justify-content:center; }
.btn:hover:not(:disabled) { border-color:var(--blue); color:var(--blue); background:var(--blue-light); }
.btn:active:not(:disabled) { transform:scale(0.97); }
.btn:disabled { opacity:0.35; cursor:not-allowed; }
.btn.primary { background:linear-gradient(135deg,var(--blue),var(--blue-mid)); border-color:var(--blue); color:white; box-shadow:0 3px 14px rgba(37,99,235,0.3); }
.btn.primary:hover:not(:disabled) { transform:translateY(-1px); box-shadow:0 6px 20px rgba(37,99,235,0.38); }
.btn.danger { border-color:rgba(220,38,38,0.25); color:var(--red); }
.btn.danger:hover:not(:disabled) { background:var(--red-bg); border-color:var(--red); }
.btn.next-btn { border-color:rgba(37,99,235,0.3); color:var(--blue); background:var(--blue-light); }
.btn.next-btn:hover:not(:disabled) { background:var(--blue-pale); border-color:var(--blue); }
#btn-prev { grid-column:1; grid-row:1; }
#btn-hint { grid-column:2; grid-row:1; }
#btn-skip { grid-column:3; grid-row:1; }
#btn-submit { grid-column:1/span 2; grid-row:2; }
#btn-next { grid-column:3; grid-row:2; }
#btn-finish { grid-column:1/span 3; grid-row:3; }
/* SCORE SCREEN */
.score-hero { text-align:center; padding:clamp(1.5rem,5vw,2.2rem) clamp(1rem,4vw,1.8rem) clamp(1.2rem,4vw,1.8rem); }
.score-eyebrow { font-size:0.66rem; text-transform:uppercase; letter-spacing:0.14em; color:var(--muted); font-weight:700; margin-bottom:0.4rem; }
.score-headline { font-family:'DM Serif Display',serif; font-size:clamp(1.5rem,5vw,2rem); font-weight:400; margin-bottom:0.9rem; }
.sources-used { display:flex; gap:0.35rem; flex-wrap:wrap; justify-content:center; margin-bottom:1.2rem; }
.src-badge { display:inline-flex; align-items:center; gap:0.3rem; background:var(--blue-light); border:1px solid var(--blue-pale); border-radius:20px; padding:0.22rem 0.75rem; font-size:0.67rem; color:var(--blue); font-weight:600; }
.score-ring-wrap { display:flex; justify-content:center; margin-bottom:1.2rem; }
.score-ring { width:clamp(100px,25vw,130px); height:clamp(100px,25vw,130px); border-radius:50%; border:4px solid var(--blue); background:radial-gradient(circle,var(--blue-light),var(--surface)); display:flex; flex-direction:column; align-items:center; justify-content:center; box-shadow:0 0 0 10px rgba(37,99,235,0.07),var(--shadow-md); }
.ring-pct { font-family:'DM Serif Display',serif; font-size:clamp(1.8rem,6vw,2.3rem); font-weight:400; color:var(--blue); line-height:1; }
.ring-lbl { font-size:0.56rem; text-transform:uppercase; letter-spacing:0.1em; color:var(--muted); margin-top:0.12rem; font-weight:700; }
.stats-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:0.5rem; margin-bottom:1.2rem; }
.stat-box { background:var(--bg2); border:1.5px solid var(--border); border-radius:12px; padding:0.8rem 0.4rem; text-align:center; }
.stat-num { font-family:'DM Serif Display',serif; font-size:clamp(1.4rem,5vw,1.75rem); font-weight:400; line-height:1; }
.stat-lbl { font-size:clamp(0.54rem,1.5vw,0.6rem); color:var(--muted); text-transform:uppercase; letter-spacing:0.08em; margin-top:0.22rem; font-weight:700; }
.stat-box.c .stat-num { color:var(--green); }
.stat-box.w .stat-num { color:var(--red); }
.stat-box.sk .stat-num { color:var(--amber); }
.stat-box.s .stat-num { color:var(--blue); }
.score-actions { display:flex; gap:0.6rem; }
.score-actions .btn { flex:1; padding:0.85rem; font-size:clamp(0.8rem,2.5vw,0.88rem); }
/* RESULT TABS */
.result-tabs-wrap { margin-top:1rem; }
.tab-bar { display:flex; background:var(--surface); border:1px solid var(--border); border-radius:12px 12px 0 0; overflow:hidden; overflow-x:auto; scrollbar-width:none; }
.tab-bar::-webkit-scrollbar { display:none; }
.tab-btn { flex:1; min-width:80px; padding:0.72rem 0.5rem; border:none; background:transparent; font-family:'DM Sans',sans-serif; font-size:clamp(0.7rem,2vw,0.77rem); font-weight:600; color:var(--muted); cursor:pointer; transition:all 0.18s; border-bottom:2.5px solid transparent; text-align:center; white-space:nowrap; min-height:var(--touch-min); }
.tab-btn:hover { color:var(--blue); background:var(--blue-light); }
.tab-btn.active { color:var(--blue); background:var(--blue-light); border-bottom-color:var(--blue); }
.tab-panel { display:none; background:var(--surface); border:1px solid var(--border); border-top:none; border-radius:0 0 12px 12px; padding:1rem; }
.tab-panel.active { display:block; animation:fadeUp 0.25s ease; }
/* REVIEW CARDS */
.review-card { background:var(--surface); border:1px solid var(--border); border-radius:12px; padding:1.1rem 1.2rem; margin-bottom:0.8rem; box-shadow:var(--shadow); }
.review-card.r-wrong { border-left:4px solid var(--red); }
.review-card.r-correct { border-left:4px solid var(--green); }
.review-card.r-skipped { border-left:4px solid var(--amber); }
.rv-q { font-size:clamp(0.82rem,2.5vw,0.88rem); font-weight:600; line-height:1.45; margin-bottom:0.55rem; color:var(--text); }
.rv-status { font-size:0.65rem; font-weight:700; padding:0.2rem 0.6rem; border-radius:20px; display:inline-block; margin-bottom:0.6rem; }
.rv-status.correct { background:var(--green-bg); color:var(--green); border:1px solid rgba(22,163,74,0.2); }
.rv-status.wrong { background:var(--red-bg); color:var(--red); border:1px solid rgba(220,38,38,0.2); }
.rv-status.skipped { background:var(--amber-bg); color:var(--amber); border:1px solid rgba(217,119,6,0.2); }
.rv-status.partial { background:var(--blue-light); color:var(--blue); border:1px solid rgba(37,99,235,0.2); }
.rv-options { display:flex; flex-direction:column; gap:0.3rem; margin-bottom:0.5rem; }
.rv-opt { display:flex; align-items:center; gap:0.55rem; padding:0.45rem 0.7rem; border-radius:8px; border:1.5px solid var(--border); background:var(--surface2); font-size:0.78rem; }
.rv-opt.opt-correct { border-color:rgba(22,163,74,0.4); background:var(--green-bg); color:var(--green); font-weight:600; }
.rv-opt.opt-wrong-pick { border-color:rgba(220,38,38,0.4); background:var(--red-bg); color:var(--red); font-weight:600; }
.rv-opt-letter { width:22px; height:22px; border-radius:5px; border:1.5px solid currentColor; display:flex; align-items:center; justify-content:center; font-size:0.62rem; font-weight:800; flex-shrink:0; opacity:0.8; }
.rv-opt-icon { margin-left:auto; font-size:0.75rem; flex-shrink:0; }
.rv-expl { font-size:0.72rem; color:var(--text2); line-height:1.55; padding:0.5rem 0.7rem; background:var(--bg2); border-radius:8px; border-left:3px solid var(--blue-pale); }
.rv-expl-label { font-size:0.6rem; text-transform:uppercase; letter-spacing:0.1em; color:var(--blue); font-weight:700; margin-bottom:0.2rem; }
.empty-note { text-align:center; padding:2rem 1rem; color:var(--muted); font-size:0.83rem; }
/* CONFETTI */
.confetti-wrap { position:fixed; inset:0; pointer-events:none; z-index:300; display:none; }
.confetti-wrap.show { display:block; }
.confetti-piece { position:absolute; border-radius:2px; animation:confetti-fall linear forwards; }
@media(min-width:768px) { body{padding:2rem 1.5rem 5rem;} .settings-grid{grid-template-columns:1fr 1fr 1fr;} }
@media(max-width:400px) { body{padding:1rem 0.75rem 5rem;} .card,.question-card{padding:1.1rem;} .settings-grid{grid-template-columns:1fr;} .stats-grid{grid-template-columns:1fr 1fr;} .brand-title{font-size:1.65rem;} .btn{padding:0.7rem 0.4rem;font-size:0.72rem;} }
@supports(padding-bottom:env(safe-area-inset-bottom)) { body{padding-bottom:calc(5rem + env(safe-area-inset-bottom));} }
</style>
</head>
<body>
<div class="wrap">
<!-- LOADING -->
<div class="loading-overlay" id="loading-overlay">
<div class="loading-orb">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/>
</svg>
</div>
<div class="loading-title">Generating Your Exam</div>
<div class="loading-sub" id="loading-sub">AI is reading your PDF...</div>
<div class="loading-sources" id="loading-sources"></div>
<div class="real-progress-wrap">
<div class="real-progress-track"><div class="real-progress-fill" id="real-progress-fill"></div></div>
<div class="real-progress-pct" id="real-progress-pct">0%</div>
</div>
<div class="loading-msg" id="loading-msg">Starting...</div>
<div class="scan-overlay-warn" id="scan-overlay-warn">⚠️ Scanned PDF detected — OCR processing may take 2–3 minutes. Please wait...</div>
</div>
<div class="confetti-wrap" id="confetti-wrap"></div>
<!-- UPLOAD SCREEN -->
<div class="screen active" id="screen-upload">
<div class="brand">
<div class="brand-pill">AI Powered</div>
<div class="brand-title">Learn <span>Smarter,</span><br>Score Higher</div>
<p class="brand-sub">Upload up to 3 PDFs (250 MB each) — mix topics and get exam-ready</p>
</div>
<div class="card">
<div class="card-label">Upload PDFs</div>
<div class="scan-warn" id="scan-warn">⚠️ One or more files are large. If scanned PDFs, processing may take 2–3 minutes.</div>
<div class="pdf-section">
<div class="drop-zone" id="drop-zone-main">
<input type="file" id="file-input-main" accept=".pdf" multiple>
<svg class="drop-icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
</svg>
<div class="drop-title">Drop your PDFs here</div>
<div class="drop-sub">or tap to browse — max 250 MB per file</div>
<div class="drop-badge">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:11px;height:11px"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/></svg>
1 to 3 PDFs · 250 MB each
</div>
</div>
<div class="pdf-list" id="pdf-list"></div>
<div class="pdf-add-row" id="pdf-add-row" style="display:none">
<div class="add-drop-zone" id="add-drop-zone">
<input type="file" id="file-input-add" accept=".pdf" multiple>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Add another PDF
</div>
<div class="pdf-count-badge"><span id="pdf-count-num">1</span>/3</div>
</div>
</div>
<div class="card-label">Quiz Settings</div>
<div class="settings-grid">
<div class="field">
<span class="field-label">Question Type</span>
<select id="q-type">
<option value="mcq">Multiple Choice</option>
<option value="fill">Fill in the Blanks</option>
<option value="long">Long Answer</option>
</select>
</div>
<div class="field">
<span class="field-label">Difficulty</span>
<select id="q-diff">
<option value="easy">Easy</option>
<option value="medium" selected>Medium</option>
<option value="hard">Hard (Multi-correct)</option>
</select>
</div>
<div class="field">
<span class="field-label">No. of Questions</span>
<input type="number" id="q-count" value="10" min="5" max="180">
</div>
</div>
<div class="card-label">Timer Settings</div>
<div class="timer-section-wrap">
<div class="timer-mode-tabs">
<button class="tmt-btn active" id="tmt-none" onclick="setTimerMode('none')">No Timer</button>
<button class="tmt-btn" id="tmt-total" onclick="setTimerMode('total')">Total Runtime</button>
<button class="tmt-btn" id="tmt-perq" onclick="setTimerMode('perq')">Per Question</button>
</div>
<div class="timer-panel" id="panel-total">
<div class="field-label" style="margin-bottom:0.5rem">Select total exam duration</div>
<div class="time-presets" id="total-presets">
<button class="time-btn sel" data-val="3600" onclick="selectTotalTime(this,3600)">1 hr</button>
<button class="time-btn" data-val="7200" onclick="selectTotalTime(this,7200)">2 hrs</button>
<button class="time-btn" data-val="10800" onclick="selectTotalTime(this,10800)">3 hrs</button>
<button class="time-btn" data-val="custom" onclick="selectTotalTime(this,'custom')">Custom</button>
</div>
<div class="custom-row" id="custom-total-row">
<input type="number" id="custom-total-val" placeholder="e.g. 90" min="1" max="600">
<span class="custom-unit">minutes</span>
</div>
</div>
<div class="timer-panel" id="panel-perq">
<div class="field-label" style="margin-bottom:0.5rem">Select time per question</div>
<div class="time-presets" id="perq-presets">
<button class="time-btn sel" data-val="30" onclick="selectPerqTime(this,30)">30 sec</button>
<button class="time-btn" data-val="60" onclick="selectPerqTime(this,60)">1 min</button>
<button class="time-btn" data-val="90" onclick="selectPerqTime(this,90)">90 sec</button>
<button class="time-btn" data-val="120" onclick="selectPerqTime(this,120)">2 min</button>
<button class="time-btn" data-val="custom" onclick="selectPerqTime(this,'custom')">Custom</button>
</div>
<div class="custom-row" id="custom-perq-row">
<input type="number" id="custom-perq-val" placeholder="e.g. 45" min="5" max="600">
<span class="custom-unit">seconds</span>
</div>
</div>
</div>
<button class="btn-start" id="start-btn" onclick="startQuiz()" disabled>
<div class="spin" id="start-spin"></div>
<span id="start-txt">Start Exam</span>
</button>
<div class="err-toast" id="upload-err"></div>
</div>
</div>
<!-- QUIZ SCREEN -->
<div class="screen" id="screen-quiz">
<div class="quiz-top">
<div class="score-chip" id="score-chip">Q 1/10</div>
<div class="prog-wrap">
<div class="prog-bar"><div class="prog-fill" id="prog-fill" style="width:0%"></div></div>
<div class="prog-text" id="prog-text">Question 1 of 10</div>
</div>
<div class="total-timer" id="total-timer">1:00:00</div>
<div class="perq-timer" id="perq-timer">30s</div>
</div>
<div class="question-viewport">
<div class="question-card" id="question-card">
<div class="q-meta">
<span class="tag" id="type-tag">MCQ</span>
<span class="tag medium" id="diff-tag">Medium</span>
<span class="tag bluetag" id="multi-tag" style="display:none">Multi-Select</span>
<span class="q-num" id="q-num">Q1</span>
</div>
<div class="q-text" id="q-text">Loading...</div>
<div class="multi-notice" id="multi-notice">Select ALL correct answers before submitting</div>
<div id="ans-area"></div>
<div class="hint-box" id="hint-box"></div>
<div class="skipped-badge" id="skipped-badge">This question was skipped</div>
</div>
</div>
<div class="actions-grid">
<button class="btn" id="btn-prev" onclick="doPrev()">Previous</button>
<button class="btn" id="btn-hint" onclick="getHint()">Hint</button>
<button class="btn" id="btn-skip" onclick="doSkip()">Skip</button>
<button class="btn primary" id="btn-submit" onclick="doSubmit()">Submit Answer</button>
<button class="btn next-btn" id="btn-next" onclick="doNext()">Next →</button>
<button class="btn danger" id="btn-finish" onclick="confirmFinish()">Finish Exam</button>
</div>
</div>
<!-- SCORE SCREEN -->
<div class="screen" id="screen-score">
<div class="card score-hero">
<div class="score-eyebrow">Exam Complete</div>
<div class="score-headline" id="score-headline">Well Done!</div>
<div class="sources-used" id="sources-used"></div>
<div class="score-ring-wrap">
<div class="score-ring">
<div class="ring-pct" id="ring-pct">0%</div>
<div class="ring-lbl">Score</div>
</div>
</div>
<div class="stats-grid">
<div class="stat-box c" ><div class="stat-num" id="s-correct">0</div><div class="stat-lbl">Correct</div></div>
<div class="stat-box w" ><div class="stat-num" id="s-wrong">0</div><div class="stat-lbl">Wrong</div></div>
<div class="stat-box sk"><div class="stat-num" id="s-skipped">0</div><div class="stat-lbl">Skipped</div></div>
<div class="stat-box s" ><div class="stat-num" id="s-score">0</div><div class="stat-lbl">Points</div></div>
</div>
<div class="score-actions">
<button class="btn" onclick="doExport()">Export PDF</button>
<button class="btn primary" onclick="doRestart()">New Exam</button>
</div>
</div>
<div class="result-tabs-wrap">
<div class="tab-bar">
<button class="tab-btn active" id="tab-all" onclick="switchTab('all')">All</button>
<button class="tab-btn" id="tab-correct" onclick="switchTab('correct')">Correct</button>
<button class="tab-btn" id="tab-wrong" onclick="switchTab('wrong')">Wrong</button>
<button class="tab-btn" id="tab-skipped" onclick="switchTab('skipped')">Skipped</button>
</div>
<div class="tab-panel active" id="panel-all"></div>
<div class="tab-panel" id="panel-correct"></div>
<div class="tab-panel" id="panel-wrong"></div>
<div class="tab-panel" id="panel-skipped"></div>
</div>
</div>
</div>
<script>
const API = window.location.origin;
const MAX_PDFS = 3;
const MAX_MB = 250;
let selectedFiles = [];
let sessionId = null;
let sourcesUsed = [];
let timerMode = "none";
let totalSeconds = 3600;
let perqSeconds = 30;
let totalInterval = null;
let perqInterval = null;
let totalRemaining = 0;
let currentAnswer = null;
let answered = false;
let skipped = false;
let isMulti = false;
let selectedOpts = new Set();
let currentQNum = 1;
let totalQNum = 10;
let answersCache = {};
let currentQId = null;
let pollTimer = null;
// ── File Handling ──────────────────────────────────────────────────────────────
const mainInput = document.getElementById("file-input-main");
const addInput = document.getElementById("file-input-add");
const mainDrop = document.getElementById("drop-zone-main");
const addDrop = document.getElementById("add-drop-zone");
mainInput.addEventListener("change", e => addFiles(Array.from(e.target.files)));
addInput.addEventListener("change", e => addFiles(Array.from(e.target.files)));
mainDrop.addEventListener("dragover", e => { e.preventDefault(); mainDrop.classList.add("over"); });
mainDrop.addEventListener("dragleave", () => mainDrop.classList.remove("over"));
mainDrop.addEventListener("drop", e => { e.preventDefault(); mainDrop.classList.remove("over"); addFiles(Array.from(e.dataTransfer.files)); });
addDrop.addEventListener("dragover", e => { e.preventDefault(); addDrop.classList.add("over"); });
addDrop.addEventListener("dragleave", () => addDrop.classList.remove("over"));
addDrop.addEventListener("drop", e => { e.preventDefault(); addDrop.classList.remove("over"); addFiles(Array.from(e.dataTransfer.files)); });
function addFiles(files) {
const pdfs = files.filter(f => f.name.toLowerCase().endsWith(".pdf"));
if (!pdfs.length) { showErr("Please select PDF files only."); return; }
let sizeErr = [];
for (const f of pdfs) {
if (selectedFiles.length >= MAX_PDFS) break;
if (selectedFiles.find(sf => sf.name===f.name && sf.size===f.size)) continue;
if (f.size > MAX_MB*1024*1024) { sizeErr.push(f.name); continue; }
selectedFiles.push(f);
}
if (sizeErr.length) showErr(`File(s) exceed ${MAX_MB}MB: ${sizeErr.join(", ")}`);
mainInput.value=""; addInput.value="";
renderFileList();
}
function removeFile(i) { selectedFiles.splice(i,1); renderFileList(); }
function renderFileList() {
const list=document.getElementById("pdf-list");
const mainZone=document.getElementById("drop-zone-main");
const addRow=document.getElementById("pdf-add-row");
const countNum=document.getElementById("pdf-count-num");
const scanWarn=document.getElementById("scan-warn");
list.innerHTML="";
if (!selectedFiles.length) {
mainZone.style.display="block"; addRow.style.display="none"; scanWarn.classList.remove("show");
} else {
mainZone.style.display="none";
addRow.style.display=selectedFiles.length<MAX_PDFS?"flex":"none";
countNum.textContent=selectedFiles.length;
let hasLarge=false;
selectedFiles.forEach((f,i) => {
const sizeMB=(f.size/1048576).toFixed(1);
if (f.size>50*1024*1024) hasLarge=true;
const item=document.createElement("div"); item.className="pdf-item";
item.innerHTML=
"<div class='pdf-icon-wrap'><svg viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><path d='M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z'/><polyline points='14,2 14,8 20,8'/></svg></div>"+
"<div class='pdf-info'><div class='pdf-name'>"+escHtml(f.name)+"</div><div class='pdf-meta'>"+sizeMB+" MB</div></div>"+
"<span class='pdf-status ok'>Ready</span>"+
"<button class='pdf-remove' onclick='removeFile("+i+")'><svg viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round'><line x1='18' y1='6' x2='6' y2='18'/><line x1='6' y1='6' x2='18' y2='18'/></svg></button>";
list.appendChild(item);
});
scanWarn.classList.toggle("show", hasLarge);
}
document.getElementById("start-btn").disabled=!selectedFiles.length;
document.getElementById("start-txt").textContent=selectedFiles.length<=1?"Start Exam":"Start Exam — "+selectedFiles.length+" PDFs";
document.getElementById("upload-err").style.display="none";
}
function escHtml(s) { return s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"); }
// ── Timer Mode ─────────────────────────────────────────────────────────────────
function setTimerMode(mode) {
timerMode=mode;
["none","total","perq"].forEach(m=>document.getElementById("tmt-"+m).classList.toggle("active",m===mode));
document.getElementById("panel-total").classList.toggle("show",mode==="total");
document.getElementById("panel-perq").classList.toggle("show",mode==="perq");
}
function selectTotalTime(btn,val) {
document.querySelectorAll("#total-presets .time-btn").forEach(b=>b.classList.remove("sel")); btn.classList.add("sel");
const row=document.getElementById("custom-total-row");
if (val==="custom"){row.classList.add("show");totalSeconds=(parseInt(document.getElementById("custom-total-val").value)||60)*60;}
else{row.classList.remove("show");totalSeconds=val;}
}
document.getElementById("custom-total-val").addEventListener("input",e=>{totalSeconds=(parseInt(e.target.value)||60)*60;});
function selectPerqTime(btn,val) {
document.querySelectorAll("#perq-presets .time-btn").forEach(b=>b.classList.remove("sel")); btn.classList.add("sel");
const row=document.getElementById("custom-perq-row");
if (val==="custom"){row.classList.add("show");perqSeconds=parseInt(document.getElementById("custom-perq-val").value)||30;}
else{row.classList.remove("show");perqSeconds=val;}
}
document.getElementById("custom-perq-val").addEventListener("input",e=>{perqSeconds=parseInt(e.target.value)||30;});
// ── Screen Switch ──────────────────────────────────────────────────────────────
function showScreen(id) {
const cur=document.querySelector(".screen.active"),next=document.getElementById(id);
if (!cur||cur===next){next.classList.add("active");return;}
cur.classList.add("slide-exit");
setTimeout(()=>{
cur.classList.remove("active","slide-exit");
next.classList.add("active","slide-enter");
setTimeout(()=>next.classList.remove("slide-enter"),400);
},320);
window.scrollTo(0,0);
}
// ── Start Quiz ─────────────────────────────────────────────────────────────────
async function startQuiz() {
if (!selectedFiles.length) return;
const lSources=document.getElementById("loading-sources");
lSources.innerHTML="";
selectedFiles.forEach(f=>{
const b=document.createElement("span"); b.className="ls-badge";
b.textContent=f.name.length>24?f.name.substring(0,22)+"...":f.name;
lSources.appendChild(b);
});
document.getElementById("loading-sub").textContent=selectedFiles.length===1?"AI is reading your PDF...":"AI is combining "+selectedFiles.length+" PDFs...";
document.getElementById("real-progress-fill").style.width="0%";
document.getElementById("real-progress-pct").textContent="0%";
document.getElementById("loading-msg").textContent="Uploading files...";
document.getElementById("scan-overlay-warn").classList.remove("show");
document.getElementById("loading-overlay").classList.add("show");
document.getElementById("upload-err").style.display="none";
const fd=new FormData();
selectedFiles.forEach(f=>fd.append("files",f));
fd.append("question_type",document.getElementById("q-type").value);
fd.append("difficulty",document.getElementById("q-diff").value);
fd.append("count",document.getElementById("q-count").value);
fd.append("exam_mode",true);
fd.append("time_limit",timerMode==="perq"?perqSeconds:0);
try {
const res=await fetch(API+"/generate",{method:"POST",body:fd});
const data=await res.json();
if (data.job_id) { pollJob(data.job_id); }
else { document.getElementById("loading-overlay").classList.remove("show"); showErr(data.detail||"Could not start. Try again."); }
} catch(e) {
document.getElementById("loading-overlay").classList.remove("show");
showErr("Cannot reach server. Make sure the Space is running.");
}
}
function pollJob(jobId) {
clearInterval(pollTimer);
pollTimer=setInterval(async()=>{
try {
const res=await fetch(API+"/job/"+jobId);
const data=await res.json();
const pct=data.progress||0;
document.getElementById("real-progress-fill").style.width=pct+"%";
document.getElementById("real-progress-pct").textContent=pct+"%";
document.getElementById("loading-msg").textContent=data.message||"";
if (data.scanned_warn) document.getElementById("scan-overlay-warn").classList.add("show");
if (data.status==="done") {
clearInterval(pollTimer);
document.getElementById("loading-overlay").classList.remove("show");
sessionId=data.session_id; sourcesUsed=data.sources||selectedFiles.map(f=>f.name);
answersCache={};
showScreen("screen-quiz");
loadQuestion(1,"right");
startTotalTimer();
} else if (data.status==="error") {
clearInterval(pollTimer);
document.getElementById("loading-overlay").classList.remove("show");
showErr(data.message||"Generation failed. Please try again.");
}
} catch(e){console.error("Poll error:",e);}
},1500);
}
// ── Timers ─────────────────────────────────────────────────────────────────────
function startTotalTimer() {
clearInterval(totalInterval);
if (timerMode!=="total") return;
totalRemaining=totalSeconds;
const el=document.getElementById("total-timer");
el.style.display="block"; el.className="total-timer";
updateTotalDisplay();
totalInterval=setInterval(()=>{
totalRemaining--;
updateTotalDisplay();
if (totalRemaining<=300) el.className="total-timer warn";
if (totalRemaining<=60) el.className="total-timer hot";
if (totalRemaining<=0) {clearInterval(totalInterval);showResults();}
},1000);
}
function updateTotalDisplay() {
const h=Math.floor(totalRemaining/3600),m=Math.floor((totalRemaining%3600)/60),s=totalRemaining%60;
document.getElementById("total-timer").textContent=h>0?h+":"+(m<10?"0":"")+m+":"+(s<10?"0":"")+s:m+":"+(s<10?"0":"")+s;
}
function startPerqTimer() {
clearInterval(perqInterval);
if (timerMode!=="perq") return;
let left=perqSeconds;
const el=document.getElementById("perq-timer");
el.style.display="block"; el.textContent=left+"s"; el.className="perq-timer";
perqInterval=setInterval(()=>{
left--;
el.textContent=left+"s";
if (left<=10) el.className="perq-timer hot";
if (left<=0) {clearInterval(perqInterval);autoSkip();}
},1000);
}
function stopPerqTimer(){clearInterval(perqInterval);document.getElementById("perq-timer").style.display="none";}
function autoSkip(){if(!answered&&!skipped) doSkip(true);}
// ── Slide ──────────────────────────────────────────────────────────────────────
function slideCard(dir,cb){
const card=document.getElementById("question-card");
const outCls=dir==="right"?"q-slide-out-left":"q-slide-out-right";
const inCls =dir==="right"?"q-slide-in-right":"q-slide-in-left";
card.classList.add(outCls);
setTimeout(()=>{
card.classList.remove(outCls); cb();
card.classList.add(inCls);
card.addEventListener("animationend",()=>card.classList.remove(inCls),{once:true});
},320);
}
// ── Load Question ──────────────────────────────────────────────────────────────
async function loadQuestion(qNum,dir){
currentQNum=qNum; stopPerqTimer();
answered=false; skipped=false; currentAnswer=null; isMulti=false; selectedOpts=new Set(); currentQId=null;
document.getElementById("hint-box").style.display="none";
document.getElementById("hint-box").textContent="";
document.getElementById("skipped-badge").style.display="none";
document.getElementById("multi-notice").style.display="none";
document.getElementById("multi-tag").style.display="none";
const cached=answersCache[qNum];
if (cached && cached.answered) {
// ✅ Only lock if truly submitted — skipped questions can be re-answered
answered=true; currentAnswer=cached.answer; currentQId=cached.question_id;
}
updateActionBtns();
try {
const res=await fetch(API+"/question/"+sessionId+"?q="+qNum);
const data=await res.json();
if (data.finished||data.error){showResults();return;}
totalQNum=data.total_questions;
currentQId=data.question_id;
document.getElementById("prog-fill").style.width=((qNum-1)/totalQNum*100)+"%";
document.getElementById("prog-text").textContent="Question "+qNum+" of "+totalQNum;
document.getElementById("score-chip").textContent="Q "+qNum+"/"+totalQNum;
document.getElementById("q-num").textContent="Q"+qNum;
document.getElementById("type-tag").textContent=data.type.toUpperCase();
const dt=document.getElementById("diff-tag");
dt.className="tag "+data.difficulty;
dt.textContent=data.difficulty.charAt(0).toUpperCase()+data.difficulty.slice(1);
document.getElementById("q-text").textContent=data.question;
isMulti=data.is_multi||false;
if (isMulti){
document.getElementById("multi-notice").style.display="block";
document.getElementById("multi-tag").style.display="inline-flex";
}
const area=document.getElementById("ans-area");
area.innerHTML="";
if (data.type==="mcq"&&data.options){
const list=document.createElement("div"); list.className="opts";
Object.entries(data.options).forEach(([k,v])=>{
const btn=document.createElement("button");
btn.className="opt"; btn.dataset.key=k;
btn.innerHTML="<span class='opt-letter'>"+k+"</span>"+escHtml(v);
if (answered&&cached?.answer){
const keys=cached.answer.split(",");
if (keys.includes(k)) btn.classList.add("sel");
}
btn.onclick=()=>{
if (answered) return;
if (isMulti){
if (selectedOpts.has(k)){selectedOpts.delete(k);btn.classList.remove("sel");}
else{selectedOpts.add(k);btn.classList.add("sel");}
currentAnswer=Array.from(selectedOpts).sort().join(",");
} else {
document.querySelectorAll(".opt").forEach(b=>b.classList.remove("sel"));
btn.classList.add("sel"); currentAnswer=k;
}
};
list.appendChild(btn);
});
area.appendChild(list);
} else if (data.type==="fill"){
const inp=document.createElement("input");
inp.type="text"; inp.className="ans-input"; inp.placeholder="Type your answer here...";
if (answered&&cached?.answer) inp.value=cached.answer;
inp.oninput=e=>{currentAnswer=e.target.value;};
if (answered) inp.disabled=true;
area.appendChild(inp);
if (!answered) setTimeout(()=>inp.focus(),150);
} else {
const ta=document.createElement("textarea");
ta.className="ans-ta"; ta.placeholder="Write your detailed answer here...";
if (answered&&cached?.answer) ta.value=cached.answer;
ta.oninput=e=>{currentAnswer=e.target.value;};
if (answered) ta.disabled=true;
area.appendChild(ta);
if (!answered) setTimeout(()=>ta.focus(),150);
}
updateActionBtns();
startPerqTimer();
} catch(e){console.error(e);}
}
// ── Action Buttons ─────────────────────────────────────────────────────────────
function updateActionBtns(){
// ✅ Previous — only disabled on Q1
document.getElementById("btn-prev").disabled=currentQNum<=1;
// ✅ Skip — disabled only if already submitted
document.getElementById("btn-skip").disabled=answered;
// ✅ Submit — disabled if already submitted
const sub=document.getElementById("btn-submit");
sub.disabled=answered;
sub.textContent=answered?"Submitted":"Submit Answer";
// ✅ Next — ALWAYS enabled, never disabled regardless of state
const nextBtn=document.getElementById("btn-next");
nextBtn.disabled=currentQNum>=totalQNum;
// Show next prominently when answered or skipped
if (answered||answersCache[currentQNum]?.skipped){
nextBtn.classList.add("primary");
nextBtn.classList.remove("next-btn");
} else {
nextBtn.classList.remove("primary");
nextBtn.classList.add("next-btn");
}
}
// ✅ Previous — always navigates, no locks
function doPrev(){
if (currentQNum<=1) return;
slideCard("left",()=>loadQuestion(currentQNum-1,"left"));
}
// ✅ Next — always navigates, never gets stuck
function doNext(){
if (currentQNum>=totalQNum) return;
slideCard("right",()=>loadQuestion(currentQNum+1,"right"));
}
function doSkip(auto=false){
if (answered) return;
skipped=true; answersCache[currentQNum]={answered:false,skipped:true,answer:null,question_id:currentQId};
const fd=new FormData();
fd.append("user_answer",""); fd.append("question_id",currentQId||""); fd.append("bookmarked",false); fd.append("timed_out",auto);
fetch(API+"/answer/"+sessionId,{method:"POST",body:fd});
stopPerqTimer();
// ✅ Auto advance after skip but don't lock
if (currentQNum<totalQNum) slideCard("right",()=>loadQuestion(currentQNum+1,"right"));
else showResults();
}
async function doSubmit(){
if (answered||!currentAnswer) return;
answered=true; answersCache[currentQNum]={answered:true,skipped:false,answer:currentAnswer,question_id:currentQId};
stopPerqTimer(); updateActionBtns();
document.querySelectorAll(".opt").forEach(b=>b.disabled=true);
const inp=document.querySelector(".ans-input"),ta=document.querySelector(".ans-ta");
if (inp) inp.disabled=true; if (ta) ta.disabled=true;
const fd=new FormData();
fd.append("user_answer",currentAnswer);
fd.append("question_id",currentQId||"");
fd.append("bookmarked",false);
fd.append("timed_out",false);
try{await fetch(API+"/answer/"+sessionId,{method:"POST",body:fd});}
catch(e){console.error(e);}
// ✅ Auto advance after submit
setTimeout(()=>{
if (currentQNum<totalQNum) slideCard("right",()=>loadQuestion(currentQNum+1,"right"));
},500);
}
async function getHint(){
const box=document.getElementById("hint-box");
box.style.display="block"; box.textContent="Getting hint...";
try{
const res=await fetch(API+"/hint/"+sessionId+"?q="+currentQNum);
const data=await res.json(); box.textContent="💡 "+data.hint;
}catch{box.textContent="Could not get hint right now.";}
}
function confirmFinish(){
const unanswered=totalQNum-Object.keys(answersCache).length;
if (unanswered>0){if(!confirm("You have "+unanswered+" unanswered question(s). Finish anyway?")) return;}
clearInterval(totalInterval); clearInterval(perqInterval); showResults();
}
// ── Results ────────────────────────────────────────────────────────────────────
async function showResults(){
clearInterval(totalInterval); clearInterval(perqInterval);
showScreen("screen-score");
const srcWrap=document.getElementById("sources-used");
srcWrap.innerHTML="";
sourcesUsed.forEach(name=>{
const b=document.createElement("span"); b.className="src-badge";
b.innerHTML="<svg viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' style='width:10px;height:10px'><path d='M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z'/><polyline points='14,2 14,8 20,8'/></svg>"+escHtml(name.length>26?name.substring(0,24)+"...":name);
srcWrap.appendChild(b);
});
try{
const res=await fetch(API+"/score/"+sessionId);
const data=await res.json();
const pct=data.percentage;
document.getElementById("ring-pct").textContent=pct+"%";
document.getElementById("s-correct").textContent=data.correct;
document.getElementById("s-wrong").textContent=data.wrong;
document.getElementById("s-skipped").textContent=data.skipped||0;
document.getElementById("s-score").textContent=data.final_score;
let h="Keep Practising!";
if (pct===100){h="Perfect Score!";fireConfetti();}
else if (pct>=90) h="Outstanding!";
else if (pct>=75) h="Well Done!";
else if (pct>=50) h="Good Effort!";
document.getElementById("score-headline").textContent=h;
const history=data.history||[];
buildTab("all", history,["correct","wrong","skipped"]);
buildTab("correct",history,["correct"]);
buildTab("wrong", history,["wrong"]);
buildTab("skipped",history,["skipped"]);
}catch(e){console.error(e);}
}
function switchTab(tab){
["all","correct","wrong","skipped"].forEach(t=>{
document.getElementById("tab-"+t).classList.toggle("active",t===tab);
document.getElementById("panel-"+t).classList.toggle("active",t===tab);
});
}
function buildTab(name,history,types){
const panel=document.getElementById("panel-"+name);
panel.innerHTML="";
const items=history.filter(h=>{
const isCorrect=h.correct||h.partial;
const isSkipped=!h.user_answer||h.timed_out;
const isWrong=!h.correct&&!h.partial&&!h.timed_out&&h.user_answer;
if (types.includes("correct")&&isCorrect) return true;
if (types.includes("wrong")&&isWrong) return true;
if (types.includes("skipped")&&isSkipped) return true;
return false;
});
if (!items.length){panel.innerHTML="<div class='empty-note'>Nothing to show here.</div>";return;}
items.forEach((item,idx)=>{
const isCorrect=item.correct||item.partial;
const isSkipped=!item.user_answer||item.timed_out;
const isWrong=!isCorrect&&!isSkipped;
const div=document.createElement("div");
div.className="review-card "+(isCorrect?"r-correct":isSkipped?"r-skipped":"r-wrong");
let statusLabel,statusClass;
if (isSkipped){statusLabel="Skipped";statusClass="skipped";}
else if (item.partial){statusLabel="Partial";statusClass="partial";}
else if (isCorrect){statusLabel="Correct";statusClass="correct";}
else{statusLabel="Wrong";statusClass="wrong";}
let inner="<div class='rv-q'>Q"+(idx+1)+". "+escHtml(item.question)+"</div>";
inner+="<span class='rv-status "+statusClass+"'>"+statusLabel+"</span>";
const options=item.options||{};
if (Object.keys(options).length>0){
inner+="<div class='rv-options'>";
const correctKeys=item.correct_answer?item.correct_answer.split(",").map(k=>k.trim().toUpperCase()):[];
const userKeys=item.user_answer?item.user_answer.split(",").map(k=>k.trim().toUpperCase()):[];
Object.entries(options).forEach(([key,val])=>{
const k=key.trim().toUpperCase();
const isCorrectOpt=correctKeys.includes(k);
const isUserPick=userKeys.includes(k);
let optClass="rv-opt";
let icon="";
if (isCorrectOpt&&isUserPick){optClass+=" opt-correct opt-user-correct";icon="✅";}
else if (isCorrectOpt){optClass+=" opt-correct";icon="✅";}
else if (isUserPick&&isWrong){optClass+=" opt-wrong-pick";icon="❌";}
inner+="<div class='"+optClass+"'><span class='rv-opt-letter'>"+key+"</span>"+escHtml(val)+"<span class='rv-opt-icon'>"+icon+"</span></div>";
});
inner+="</div>";
} else {
if (item.user_answer) inner+="<div class='rv-opt "+(isCorrect?"opt-correct":"opt-wrong-pick")+"'><span class='rv-opt-letter'>You</span>"+escHtml(item.user_answer)+"</div>";
if (!isCorrect||isSkipped) inner+="<div class='rv-opt opt-correct'><span class='rv-opt-letter'>Ans</span>"+escHtml(item.correct_answer||"")+"</div>";
}
if (item.explanation){
inner+="<div class='rv-expl'><div class='rv-expl-label'>Explanation</div>"+escHtml(item.explanation)+"</div>";
}
div.innerHTML=inner;
panel.appendChild(div);
});
}
function fireConfetti(){
const wrap=document.getElementById("confetti-wrap");
const colors=["#2563eb","#3b82f6","#93c5fd","#16a34a","#dc2626","#d97706","#7c3aed"];
wrap.innerHTML=""; wrap.classList.add("show");
for(let i=0;i<80;i++){
const p=document.createElement("div"); p.className="confetti-piece";
p.style.cssText=`left:${Math.random()*100}%;background:${colors[Math.floor(Math.random()*colors.length)]};width:${6+Math.random()*8}px;height:${6+Math.random()*8}px;border-radius:${Math.random()>.5?"50%":"2px"};animation-duration:${2+Math.random()*2}s;animation-delay:${Math.random()}s;`;
wrap.appendChild(p);
}
setTimeout(()=>{wrap.classList.remove("show");wrap.innerHTML="";},4000);
}
async function doExport(){
try{
const res=await fetch(API+"/export/"+sessionId);
if(res.ok){
const blob=await res.blob();
const url=URL.createObjectURL(blob);
const a=document.createElement("a"); a.href=url; a.download="results_"+sessionId+".pdf";
document.body.appendChild(a); a.click(); document.body.removeChild(a);
URL.revokeObjectURL(url);
} else{alert("Export failed. Please try again.");}
}catch{alert("Export failed. Please try again.");}
}
function doRestart(){
sessionId=null; selectedFiles=[]; sourcesUsed=[]; answersCache={};
answered=false; skipped=false; currentAnswer=null; currentQId=null;
clearInterval(totalInterval); clearInterval(perqInterval); clearInterval(pollTimer);
mainInput.value=""; addInput.value="";
document.getElementById("total-timer").style.display="none";
document.getElementById("perq-timer").style.display="none";
setTimerMode("none");
renderFileList();
showScreen("screen-upload");
}
function showErr(msg){
const el=document.getElementById("upload-err");
el.textContent=msg; el.style.display="block";
}
</script>
</body>
</html>