Study cards: from-your-notes, read aloud, timed runs

From your notes: the generator now stores a verbatim source quote with every
activity (schema + prompt + a server-side clamp so a chatty model cannot drop a
card), and the feedback panel shows the sentence that carries it with the quote
highlighted green. Decks made before the change, and quotes the model trimmed,
fall back to a local IDF-weighted match over the saved notes text
(lib/notes-source.ts): nothing is shown unless at least two distinct terms
land. The passage hides in exam mode with the other reveals.

Read aloud: a Web Speech API speaker control on the text a learner reads
(flashcard faces, questions, statements, sentences, instructions) via
lib/speech.ts and components/SpeakButton.tsx — one utterance app-wide, a voice
matched to the deck's language, and no button at all where the browser has no
speech synthesis. It used to sit inside the flashcard's flip button (nested
buttons, tapping it flipped the card); it now lives next to the hint row.

Timed runs: the feed chrome carries a timer chip (off by default) that gives
each card 30 seconds; a card that runs out is recorded as a miss through the
same path as a wrong answer (history + SRS agree), shows a toast, and the feed
moves straight on with a fresh budget. handleAnswered ignores a second result
per card, so a timeout plus a manual answer cannot record twice.

The generator's type spec now names what each type suits (language decks lean
on flashcards/matching/fill blank, ordering needs a real sequence, short answer
reasoning), so the deck-aware guidance covers all seven types.

New activities-test suite (24 checks: quote path, local path, the green mark,
the speech stub, the timed run on Playwright's fake clock), registered in
run-all; phase2 pins that the notes passage stays hidden in exam mode; en+cs
keys for every new string; no new dependencies. Version 0.6.1, SW v83.
This commit is contained in:
Martin Dvořák 2026-10-15 05:53:47 +00:00
commit 796e2e9fe9
26 changed files with 1068 additions and 51 deletions

View file

@ -172,6 +172,13 @@ no database, no data leaving your machine except the requests you send to Ollama
| `ordering` | Drag with the mouse/finger, or use ▲▼ buttons, then check |
| `short_answer` | Typed answer graded by the AI via `/api/grade`, with a self-check fallback |
Every card carries the extras that make it studyable: a **speaker** control (Web Speech API,
the deck's language, on the text you are meant to read — flashcard faces, questions, statements,
instructions), a **“From your notes”** passage after you answer (the generator stores a verbatim
quote with each activity; decks made before that get a local, IDF-weighted sentence match over
the saved notes) with the matching words highlighted green, and the feed chrome's **⏱ timed run**
(off by default — 30 seconds a card, running out counts as a miss and the feed moves on).
**First run**
- A full-screen **API-key gate** replaces the app until a key is saved (in `user-key` mode): one
@ -846,11 +853,13 @@ app/
components/
activities/ one component per activity type + shared shell
Flashcard.tsx MultipleChoice.tsx TrueFalse.tsx FillBlank.tsx
Matching.tsx Ordering.tsx ShortAnswer.tsx shared.tsx index.tsx
Matching.tsx Ordering.tsx ShortAnswer.tsx shared.tsx context.tsx
NotesSource.tsx index.tsx
homework/ make-my-homework screens (input, result, guide feed, history)
chat/ chat screen, composer, message rows, markdown + math, pickers
UploadScreen.tsx StudyFeed.tsx ResultsScreen.tsx DeckCard.tsx ApiKeyGate.tsx
InstallSheet.tsx TabBar.tsx ServiceWorkerRegistrar.tsx FolderBar.tsx
SpeakButton.tsx (read-aloud control for study cards)
LibraryScreen.tsx SettingsDialog.tsx AppHeader.tsx ThemeToggle.tsx icons.tsx ui.tsx
lib/
ollama.ts server-only HTTP client (timeouts, auth, error mapping)
@ -871,6 +880,8 @@ lib/
homework.ts localStorage homework results + solution assembly
use-notes-intake.ts shared client-side photo/document intake hook
clipboard.ts copy helper with a non-secure-context fallback
notes-source.ts "from your notes": quote lookup + local sentence matching
speech.ts Web Speech API wrapper (one utterance, deck language)
image.ts client-side validation, resize and compression
client-api.ts browser client for the API routes (NDJSON reader)
chat.ts chat types + last-model memory
@ -1423,6 +1434,39 @@ 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".
92. **Study cards: where the answer came from, read aloud, and timed runs.** Three additions to
the study feed, all local and dependency-free.
- **From your notes.** The generator now returns `source` with every activity: the verbatim
words from the notes that prove the answer (max 25 words; `clampQuote` in `lib/ai.ts` trims
anything longer to a clean cut, so a chatty model cannot drop a card). After an answer the
feedback panel shows the sentence that carries it with the quote highlighted green
(`mark`). Decks made before the change — and quotes the model trimmed — fall back to a
local, IDF-weighted match over the stored notes text (`lib/notes-source.ts`): a query from
the activity's own wording, sentence scoring where rare words weigh more, word-boundary
highlighting, and nothing shown unless at least two distinct terms land (a random sentence
would be worse than silence). It disappears in exam mode with the other reveals.
- **Read aloud.** A speaker control on the text a learner is meant to read (both flashcard
faces, the visible one only; questions, statements, sentences, instructions) on the Web
Speech API (`lib/speech.ts`, `components/SpeakButton.tsx`): one utterance at a time
app-wide, a voice matched to the deck's language (BCP-47 table, interface locale as the
fallback, 0.95 rate), and no button at all where the browser has no speech synthesis.
Tapping the speaker used to flip the flashcard — it sat inside the flip button (nested
buttons) — so it now lives next to the hint row, outside the flip target.
- **Timed runs.** The feed chrome carries a timer chip (off by default): on, each card gets
30 seconds (`TIMED_CARD_SECONDS`), the chip counts down, and a card that runs out is
recorded as a miss through the same path as a wrong answer (history + SRS agree), shows a
toast, and the feed moves straight on to the next card with a fresh budget. The auto-advance
is deliberately not deferred: recording the miss re-renders the feed and would cancel a
queued scroll with its effect cleanup. `handleAnswered` now ignores a second result for the
same card, so a timeout followed by a manual answer cannot write two history entries.
- The generator's type spec now names what each type suits (flashcards, matching and fill in
the blank for language decks; ordering for real sequences; short answer for reasoning), so
the deck-aware guidance covers all seven types, not just ordering.
New suite `activities-test` (24 checks: quote path, local path, the green mark, the speech
stub — real `SpeechSynthesisUtterance`, recording `speak`/`cancel` — and the timed run with
Playwright's fake clock), registered in run-all; the exam suite pins that the notes passage
stays hidden with the other reveals; en + cs keys for every new string; no new dependencies.
91. **The chat chrome floats: one gutter, one radius scale, one glass recipe.** The header and
the composer were full-bleed bands inside the main pane — flush to the viewport's top, right
and bottom edges, with the composer's bottom corners cropped off-screen — while the rail