From 98537cee64d7a19eb7e4c76f882a9c93f075d5f1 Mon Sep 17 00:00:00 2001 From: Martin Dvorak Date: Thu, 15 Oct 2026 13:59:55 +0000 Subject: [PATCH] Skipping a card is not a mistake Scrolling through a deck without answering recorded a 0% attempt and listed every card under "Missed items" with its answer revealed. Only answered cards count now: the score is correct/answered, the attempt stores the answered count, an empty run records nothing, and the cards scrolled past wait under "Not answered (n)" - listed as prompts with "not counted as mistakes" - and "Answer the rest (n)" starts a catch-up run of just those cards (same restart path as retry-missed, so history and the SRS still see the answers). "Aced all" requires nothing left waiting too. phase2 pins the flow: all-skipped records no attempt and no mistake list; one-of-two reads "You got 1 of 1 right" + "Not answered (1)" with attempts:1, bestScore:100; the catch-up run records the remaining answer. Exam mode keeps its own rule: in a timed exam an unanswered question is a miss. Version 0.6.5, SW v87. --- README.md | 14 ++++++++ components/ResultsScreen.tsx | 64 +++++++++++++++++++++++++++++------- components/StudyFeed.tsx | 28 ++++++++++++---- lib/i18n-dict.ts | 8 +++++ package.json | 2 +- public/sw.js | 2 +- tests/phase2-test.mjs | 63 +++++++++++++++++++++++++++++++++++ 7 files changed, 161 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 99acd4d..5e9b0c3 100644 --- a/README.md +++ b/README.md @@ -1434,6 +1434,20 @@ Things I chose while building, with the reasoning: (measured: 268..1280 at a 1280 window) — a flat pane floating 40px inside the scene read as "cut off next to the sidebar". +96. **Skipping a card is not a mistake.** "You can just scroll all the way down and it counts + everything as incorrect even if you technically just skip it to do it later." The study feed's + results treated *every* card without a result as missed: swiping to the end recorded a 0% + attempt against the deck and listed all twelve cards under "Missed items" with their answers. + Now only answered cards count: the score line is `correct / answered`, the attempt records the + answered count, an empty run records nothing at all, and the cards you scrolled past appear + under **Not answered (n)** — listed as prompts, labelled "not counted as mistakes", with + **Answer the rest (n)** starting a catch-up run of exactly those cards (the same `restart` + path as retry-missed, so history and the SRS see the answers as usual). "Aced all" now also + requires that nothing was left waiting. Phase 2 pins the whole flow: all-skipped → no attempt, + no mistake list, no score line; one-of-two → "You got 1 of 1 right", "Not answered (1)", + `attempts: 1, bestScore: 100`; the catch-up run records the remaining answer. Exam mode keeps + its own rule — in a timed exam an unanswered question *is* a miss. + 95. **Aero, second pass: a heavier shared blur, and a field that reads as a field.** Two follow-ups on 94. The composer's input was still a flat 60% white bar (the "clean, even tile" from 91) on a panel that had — rightly — become translucent, so it read as a white slab: "especially the diff --git a/components/ResultsScreen.tsx b/components/ResultsScreen.tsx index 49f7481..820fbd5 100644 --- a/components/ResultsScreen.tsx +++ b/components/ResultsScreen.tsx @@ -9,7 +9,16 @@ import type { Activity, Deck } from "@/lib/schemas"; import { downloadDeck } from "@/lib/storage"; import { useIsPhone } from "@/lib/use-phone"; import { cn } from "@/lib/utils"; -import { BoltIcon, DownloadIcon, FlameIcon, HomeIcon, RefreshIcon, SnowflakeIcon, SparklesIcon } from "@/components/icons"; +import { + BoltIcon, + DownloadIcon, + FlameIcon, + HistoryIcon, + HomeIcon, + RefreshIcon, + SnowflakeIcon, + SparklesIcon, +} from "@/components/icons"; import { Button, Chip } from "@/components/ui"; import { AskButton } from "@/components/chat/AskButton"; @@ -62,9 +71,11 @@ export function ResultsScreen({ total, correct, missed, + unanswered, results, retryMode, onRetryMissed, + onStudyUnanswered, onRestart, onRegenerate, regenerating, @@ -73,12 +84,17 @@ export function ResultsScreen({ deck: Deck; xp: number; bestStreak: number; + /** Cards that were answered (right or wrong). */ total: number; correct: number; + /** Answered, but wrong. */ missed: Activity[]; + /** Scrolled past without answering — waiting, not wrong. */ + unanswered: Activity[]; results: Record; retryMode: boolean; onRetryMissed: () => void; + onStudyUnanswered: () => void; onRestart: () => void; onRegenerate: () => void; regenerating: boolean; @@ -90,7 +106,7 @@ export function ResultsScreen({ /* Phones: the score and XP count up; desktop jumps straight in. */ const percentShown = useCountUp(percent, !isPhone); const xpShown = useCountUp(xp, !isPhone); - const perfect = missed.length === 0 && total > 0; + const perfect = missed.length === 0 && unanswered.length === 0 && total > 0; const tone = percent >= 80 ? "emerald" : percent >= 50 ? "frost" : "amber"; return ( @@ -107,11 +123,13 @@ export function ResultsScreen({

{perfect ? t("results.acedAll", { n: total, title: deck.title }) - : t("results.gotOf", { - correct, - total, - retry: retryMode ? t("results.retrySuffix") : "", - })} + : total === 0 + ? t("results.noneAnswered") + : t("results.gotOf", { + correct, + total, + retry: retryMode ? t("results.retrySuffix") : "", + })}

@@ -164,21 +182,45 @@ export function ResultsScreen({
) : null} + {unanswered.length > 0 ? ( +
+

+ {t("results.unanswered", { n: unanswered.length })} +

+

{t("results.unansweredNote")}

+
    + {unanswered.map((activity) => ( +
  • + {promptFor(activity)} +
  • + ))} +
+
+ ) : null} +
{missed.length > 0 ? ( - ) : ( + ) : null} + {unanswered.length > 0 ? ( + + ) : null} + {missed.length === 0 && unanswered.length === 0 ? ( - )} - {missed.length > 0 ? ( + ) : ( - ) : null} + )}