Spaces:
Sleeping
Sleeping
| <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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""); } | |
| // ── 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> | |