diff --git a/src/views.rs b/src/views.rs index ee50865..b185202 100644 --- a/src/views.rs +++ b/src/views.rs @@ -941,7 +941,7 @@ pub fn attempt(ctx: &Ctx, user: &str, attempt_id: &str, questions: &[Question], -"#, +"#, title = esc(title), user = esc(user), n = questions.len(), @@ -1094,6 +1094,55 @@ pub fn result(ctx: &Ctx, user: &str, attempt_id: &str, share_token: &str, a: &An .collect(); let topic_buckets: Vec = a.by_topic.iter().map(|(b, _)| b.to_owned()).collect(); let neg = rows.iter().filter(|r| !r.correct && !r.selected.trim().is_empty()).count() as i32; + // Time analysis: where the clock went, from per-question times. + let avg_for = |filt: &dyn Fn(&RowView) -> bool| -> String { + let pick: Vec = rows.iter().filter(|r| filt(r) && r.time_secs > 0).map(|r| r.time_secs).collect(); + if pick.is_empty() { "—".into() } else { format!("{}s", pick.iter().sum::() / pick.len() as i64) } + }; + let timed: Vec<(usize, &RowView)> = rows + .iter() + .enumerate() + .filter(|(_, r)| r.time_secs > 0) + .collect(); + let time_table = if timed.is_empty() { + String::new() + } else { + let time_row = |(i, r): &(usize, &RowView)| { + let cls = if r.correct { "ok-t" } else if r.selected.trim().is_empty() { "dim" } else { "bad-t" }; + format!( + r#"q{n}{sub}{t}s"#, + n = i + 1, + cls = cls, + sub = esc(&r.q.subtopic), + t = r.time_secs + ) + }; + let mut sorted = timed.clone(); + sorted.sort_by_key(|(_, r)| r.time_secs); + let fast: String = sorted.iter().take(3).map(time_row).collect(); + let mut slowest = sorted.clone(); + slowest.reverse(); + let slow: String = slowest.iter().take(3).map(time_row).collect(); + format!( + r##"

time analysis

+
+

average per outcome

+ + + +
outcomeavg time
right{rt}
wrong{wt}
blank{bt}
+
+

fastest

{fast}
subtopic
+

slowest

{slow}
subtopic
+
+
"##, + rt = avg_for(&|r| r.correct), + wt = avg_for(&|r| !r.correct && !r.selected.trim().is_empty()), + bt = avg_for(&|r| r.selected.trim().is_empty()), + fast = fast, + slow = slow + ) + }; // Encouragement line: tone follows the score, like the ZenithPrep modal. let pct = fmt_pct(a.score, a.max_score); let (enc_head, enc_body) = if pct >= 75 { @@ -1111,6 +1160,7 @@ pub fn result(ctx: &Ctx, user: &str, attempt_id: &str, share_token: &str, a: &An subject-wise difficulty chapters that cost you marks + time analysis per-question analysis{mtag} all papers dashboard @@ -1134,6 +1184,7 @@ pub fn result(ctx: &Ctx, user: &str, attempt_id: &str, share_token: &str, a: &An

chapters that cost you marks

{topic}
subtopicrightwrongblank%
+{time_table}

every question

{detail}

@@ -1170,6 +1221,7 @@ pub fn result(ctx: &Ctx, user: &str, attempt_id: &str, share_token: &str, a: &An subj = bucket_rows(&a.by_subject), diff = bucket_rows(&a.by_difficulty), topic = bucket_rows(&topic_buckets), + time_table = time_table, mtag = if mistakes_pending > 0 { format!(r#" {}"#, mistakes_pending) } else { String::new() }, title_ = esc(&title), detail = detail, diff --git a/static/exam.js b/static/exam.js index d33a5e1..54e0e0a 100644 --- a/static/exam.js +++ b/static/exam.js @@ -15,10 +15,24 @@ for (const p of panes) { const box = p.querySelector('input'); if (box) entered.set(p.dataset.i, Number(box.dataset.t || 0)); - } - const visited = new Set(); + } const visited = new Set(); const saveDot = document.getElementById('save-dot'); + // Per-question stopwatch: accrues only while the pane is on screen, so + // time spent elsewhere never counts against a question. + let tickPid = null; + function startTick(pane) { + if (tickPid) clearInterval(tickPid); + const key = pane.dataset.i; + tickPid = setInterval(() => { + entered.set(key, Number(entered.get(key) || 0) + 1); + }, 1000); + } + document.addEventListener('visibilitychange', () => { + if (document.hidden && tickPid) clearInterval(tickPid); + else if (!document.hidden) startTick(panes[cur]); + }); + function questionId(pane) { const radios = [...pane.querySelectorAll('input[type=radio]')]; if (radios.length) return Number(radios[0].name.replace('q-', '')); @@ -58,6 +72,7 @@ // A pane change on a phone must start the question at the top, not halfway down. panes[cur].scrollIntoView({ block: 'nearest' }); history.replaceState(null, '', '#q' + (cur + 1)); + startTick(panes[cur]); paintProgress(); }