Bigger decks: pick up to 30 activities, built in batches

The create wizard stopped at 15 activities because the API, the prompt and
the salvage path all capped at 18: one reply must be one valid JSON object,
and past ~18 activities models truncate or drop cards. Raising the cap alone
would have shipped decks that came back short.

- The count picker offers 8 / 10 / 12 / 15 / 20 / 30; a deck's "add more" row
  gained +15. The help line says bigger decks take longer.
- /api/generate accepts up to 30 (FREEZMO_DEFAULT_ACTIVITY_COUNT now clamps
  to 30 as well) and sizes its overall deadline from the number of model
  calls instead of a fixed two.
- generateActivities splits anything above 18 evenly across calls (30 → 15 +
  15, 25 → 13 + 12) and merges them into one deck. The first call keeps the
  "add to this deck" contract; later calls are told the title, the types
  already used and the topics already covered. A normalised content signature
  drops a card that repeats an earlier one, so the same idea never ships
  twice. requested/generated still travel in the stream meta.
- phase1-test: a stub Ollama answers two 15-card batches (the second repeats
  one card on purpose) → two calls, the even split, the "already covered"
  prompt, 29 merged cards, the duplicate shipped once, title from the first
  reply, and count 31 answered 400 (59 checks).
- wizard-test: the picker offers 30, the wizard sends count: 30, and the
  30-card deck is stored and opened (99 checks).

Version 0.8.4, SW v106.
This commit is contained in:
Martin Dvořák 2026-10-28 19:11:39 +00:00
commit e3b7bbbd6c
13 changed files with 345 additions and 46 deletions

View file

@ -3,7 +3,7 @@
**Turn photos of your study notes into a swipeable feed of mini study games.**
Freezmo is a TikTok/Gizmo-style study app. You paste or upload photos of your notes, an AI reads
them and builds a deck of 8–15 interactive activities — flashcards, quizzes, matching games and
them and builds a deck of 8–30 interactive activities — flashcards, quizzes, matching games and
more — that you swipe through vertically like a feed. Every answer gives instant feedback, XP and
streaks, and the deck is saved in your browser so you can retry it later.
@ -105,7 +105,8 @@ no database, no data leaving your machine except the requests you send to Ollama
- Up to 8 photos + 6 documents per deck (max 12 images reach the model); photos are resized to max
1600 px and re-encoded as JPEG before upload
- Optional extra text (pasted notes) and a topic hint
- Easy / Medium / Hard difficulty and 8 / 10 / 12 / 15 activities per deck
- Easy / Medium / Hard difficulty and 8 / 10 / 12 / 15 / 20 / 30 activities per deck (a big deck
is built in several model calls — see the generation pipeline)
- Fun staged loading state (`Reading your notes… → Finding the key ideas… → Building your activities…`)
- Friendly error cards with a **Retry** button that point at Settings when a key is needed
@ -782,7 +783,7 @@ Limits and behaviour worth knowing:
▼ ▼
┌──────────────────────────┐ ┌───────────────────────────┐
│ browser: study feed │ ◄───────────────────── │ TEXT_MODEL │
│ JSON validated by zod │ deck JSON │ design 8–15 activities │
│ JSON validated by zod │ deck JSON │ design 8–30 activities │
└──────────────────────────┘ └───────────────────────────┘
```
@ -794,8 +795,14 @@ Limits and behaviour worth knowing:
with the validation error fed back to the model (`lib/ai.ts`).
- Individual broken activities are dropped rather than failing the whole deck; if fewer than half
the requested activities survive, the step is retried too.
- **Bigger decks are built in batches.** One model reply has to be one valid JSON object, and past
~18 activities replies get truncated or lose cards, so a count above that is split across several
calls (30 → 15 + 15, 25 → 13 + 12) with the same notes. Each call after the first is told the
deck's title, the types already used and the topics already covered, and the merged deck drops
cards whose content repeats (so the same idea never ships twice). The route's overall deadline
scales with the number of calls.
- Requests use `stream: false` with per-request timeouts and a hard overall deadline
(two sequential model calls). Errors surface as friendly messages
(the vision step plus one model call per batch). Errors surface as friendly messages
("Ollama isn't reachable, check your settings") with a retry button.
- `POST /api/generate` answers with newline-delimited JSON (`stage` / `deck` / `error` events) so the
UI can show real progress between the vision and generation steps.
@ -1513,6 +1520,22 @@ Things I chose while building, with the reasoning:
into view as you type. No strings, so no i18n; the additive change sits on top of the merged
scene-heat rework unchanged.
112. **Bigger decks are one request but several model calls.** The create wizard topped out at 15
activities (`COUNT_OPTIONS`) because the API, the prompt and the salvage path all capped at 18:
one reply has to be one valid JSON object, and past that size models start truncating or
quietly dropping cards. Raising the cap alone would have shipped decks that came back short.
The picker now offers 8 / 10 / 12 / 15 / 20 / 30 (and a deck's "add more" row gained +15); the
route accepts up to 30; and `generateActivities` splits anything above 18 evenly across calls
(30 → 15 + 15), merging them into one deck. The first call keeps the existing "add to this
deck" contract; later calls are told the title, the types already used and the topics already
covered, and a normalised content signature (`activitySignature`) drops a card that repeats an
earlier one — the stub in phase 1 returns the same first card in both batches to pin exactly
that (29 from two 15-card replies). The route's overall deadline now scales with the number of
calls, and the requested/generated pair still travels in the stream's meta. Verified by the
phase-1 suite (59 checks: the two calls, the even split, the "already covered" prompt, the
merge + dedupe, and a 31-card request answered 400) and the wizard suite (99: the picker
offers 30, the wizard sends `count: 30`, and the 30-card deck is stored and opened).
111. **The model dropdown was clipped by the card it lived in.** "The model selection in teach it
back is kinda broken at least on desktop": the pickers' desktop panels were
`position: absolute` children of whatever container held their chip. In chat that is the