Settings -> Providers takes keys for OpenAI, Anthropic (their official
OpenAI-compatibility layer), DeepSeek, Groq, OpenRouter and any custom
OpenAI-compatible endpoint. One adapter (lib/openai-compat) covers
listing, streaming chat, JSON generation and vision, and every model
flow can run on a provider: the chat picker lists each enabled
provider's models (labelled, remembered), and a "Study generation"
choice moves decks, homework, grading and attachment transcription onto
a provider model.
Keys stay in this browser exactly like the Ollama one
(freezmo.providers.v1): never stored server-side, never in a backup or
the sync payload, forwarded only with the request that needs them. The
provider test asserts the key reaches the provider while the routing
header stays on the app side.
New suite tests/providers-test.mjs (13 checks) stubs both protocols:
model listing, chat streaming, study generation and the Settings flow.
Privacy policy updated (en+cs). Contrast 0 findings, axe clean, mobile
126/126, chat 102/102, wizard 95/95, bubbles 34/34, regression 44/44,
i18n 13/13, native 33/33. Version 0.4.7, SW -> freezmo-v72.
A generated Czech literature deck reached a phone with three identical
matching definitions — the card could not be solved. The pipeline only
deduped matching terms (and ordering items), so repeated definitions and
options sailed through.
- parseDeck now rejects any card whose options, items, terms or definitions
repeat (case-insensitive), through the same drop-or-repair path as the
ordering giveaway guard: the card is dropped, or a deck left with too few
cards gets one repair round with a clear reason.
- The matching dedupe covers definitions too, and both it and the ordering
one only trim when the card still meets the schema minimum afterwards —
a shorter card would fail validation on the next read and take the whole
deck down with it (latent data-loss bug).
- The generation prompt states the uniqueness rule.
- phase-1 suite: a stub Ollama returns a deck with a repeated-definition
matching card (the reported shape), a repeated-option card and a
salvageable one; asserts the first two are dropped, the third is trimmed
to three pairs and the good cards ship (43 checks).
- README: decision 51.
Server-side only (lib/ai.ts, lib/prompts.ts) — no client bundle change, no
SW bump.
Check my work (third homework mode)
- One model call with its own system prompt marks the student's own
attempt: verdict, what's right, each issue with why + fix, and the
corrected version that keeps everything the student got right.
Unasked-for extras are never errors. The attempt is typed/pasted
(photos of handwriting are documented as unsupported) and stored in
the record so Redo re-checks the same work.
- Server: mode "check" + solution on POST /api/homework (zod, 400
server.needSolution without one) and the same NDJSON stream; new
checkHomework() pipeline with repair-once validation.
Follow-ups
- New POST /api/homework/refine (plain JSON, RATE_LIMIT_REFINE, zod,
auth before body): shorter / more detail / explain, plain text back,
language preserved.
- Shorter + More detail + Original rewrite the finished answer and the
corrected version in place (FollowUpBar); every guide step has
Explain step N differently; failed calls say so without losing text.
Turn into a study deck
- Any result hands its content (answer, corrected version or assembled
guide) to New deck through a one-shot sessionStorage seed with the
options step pre-filled.
UI
- Phone step 2 gains the third card: tapping it reveals the solution
field + submit; desktop has three cards and the solution field shows
with the mode. Homework history labels and share/card previews cover
the new mode.
Verified: new phase-3 suite 20/20 against a stub Ollama (check payload,
missing-solution 400, refine validation, phone + desktop check flows,
shorter follow-up with restore, guide explain, deck hand-off) plus
regression 74/74, mobile 52/52, declutter 116/116, wizard 80/80,
animation 8/8, native 35/35, phase1 38/38, phase2 43/43, i18n 25/25.
Key parity 734/734. Lint + build clean. SW freezmo-v27.
A short-answer case: "Why was Kryštof Harant executed?" with the student
answering "because he took part in the anti-Habsburg uprising" was failed
for not naming the year 1621 — a detail only present in the reference
answer, never asked by the question. The generator had put the date into
keyPoints, and the grader treated every key point as mandatory.
- Grading prompt: the QUESTION defines what is required. A key point
counts only when the question asks for it or the answer is wrong or
incomplete without it; extra detail in the reference answer (dates,
places, names, numbers) is a bonus — never listed as missing, never a
reason to fail. An accepted answer must return an empty "missing"
array.
- Generation spec: short_answer keyPoints must contain only the ideas
the question asks for (a "why" question requires the reason, not the
year).
- gradeShortAnswer drops any stray "missing" entries when correct is
true, so the UI can never show a gap on an accepted answer.
Verified: mock-Ollama integration test 9/9 through the real /api/grade
route (new rules sent upstream, incorrect answers keep their missing
point, accepted answers normalise to missing: []), regression 74/74,
mobile 51/51, i18n 27/27. Lint + build clean. SW freezmo-v18.
Homework (/homework — entry points in the drawer and on the create screen)
- Assignment intake reuses the study pipeline: pasted text, photos
(camera/picker, compressed in the browser) and documents (PDF/DOCX/PPTX/
ODT/text) through lib/image + lib/documents, the same vision step and the
same language guard. Raw files never leave the device — only text and JPEG
data URLs reach the server.
- Two large choice cards: “Just make it” returns the finished homework
(answer only, plain text, copy + make it again); “Explain how to make this
homework” builds an interactive guide.
- The guide is one step per screen in the study-feed snap layout:
explanation, three progressive hints (nudge -> hint -> the answer) and an
optional checkpoint rendered by the existing activity components (graded
through /api/grade). The last screen assembles the solution from the step
answers, with a copy button. Checkpoints carry xp 0 so the guide stays free
of XP/streak chrome.
- Results live in freezmo.homework.v1 with a small history (open/delete);
“Make it again” re-runs from the stored assignment text (the vision
transcription when photos were used) — no original files are stored.
- The answer and the guide are written in the assignment's language, never
translated into the UI language.
Server
- POST /api/homework: NDJSON stage events like /api/generate, the same
http-guard, auth checked before parsing, and its own RATE_LIMIT_HOMEWORK
(documented in .env.example + the README table).
- New prompts in lib/prompts.ts and zod schemas in lib/schemas.ts with the
same single repair retry; TEXT_MODEL for content, VISION_MODEL only for
photos. New shared client pieces: useNotesIntake, lib/homework store,
lib/clipboard, and a shared NDJSON reader in lib/client-api.
Also finishing the mobile polish pass from the previous request
- Phone ice scene: the top-left orb moved behind the glass header (it sat
behind the page heading/subtitle). The settings sheet's scroll cue is now a
bare chevron inside a content fade (mask) instead of a chip over the status
banner, and the API key card's note/disclosure/actions are tightened.
- theme-color now follows the in-app theme (SSR metas still fall back to the
OS preference); apple-mobile-web-app-status-bar-style stays “default”
because iOS fixes the status-bar text colour at launch.
Verified: 78/78 homework checks (text/photo/PDF, both modes, EN+CS, dark+light,
360/390px, copy, history persistence, storage privacy, API language, axe), plus
the existing suites re-run (mobile 47/47, pixel contrast 0 below AA, i18n 27/27,
desktop regression 72/72). Lint + build clean; service worker freezmo-v6.
Ordering cards
- Generation prompt now forbids dates, years, times, step numbers and
ordinals in ordering items (spec + hard rules), and the server-side
validation drops any ordering card whose items still contain them
instead of shipping a giveaway.
True/False
- The 'false ✕' swipe label was permanently visible on the card: its
opacity curve was inverted, so the clamp at rest was 1. Flipped the
range (and tightened both ranges for the elastic drag) — each label now
only fades in while swiping in that direction.
Regenerate deck
- Results screen gains a Regenerate deck button: rebuilds the activities
from the deck's stored source notes, replaces the deck in storage
(attempts/best score reset) and starts the fresh run from slide 1.
- StudyFeed tracks the current deck so the header, export and messages use
the regenerated title/activities.
- Fixed a spurious 0% attempt recorded when a regenerated deck swapped in:
scroll anchoring held the old results position, so the feed briefly
reported 5/5 with zero answers. The scroll now resets before the new
slides render, overflow-anchor is disabled on the feed, and a run with
no answers is never recorded.
Tests: new deck-fixes suite 15/15 (TF overlay at rest/drag, regenerate
with loading state + storage reset, ordering guard drop/keep); retry
14/14, deck chrome 15/15, pwa 16/16, final-ux 12/12, security 8/8,
contrast 20/20.
Next.js 15 app that turns photos, PDFs, DOCX/PPTX and text into a swipeable
feed of AI-generated activities (flashcards, multiple choice, true/false,
fill-in-the-blank, matching, ordering, short answer) using the user's own
Ollama models.
Highlights:
- All AI calls run in server route handlers; the browser never talks to Ollama.
- Bring-your-own-key mode (default): the key lives only in the browser and is
sent per-request; server, local-daemon and shared-key modes also supported.
- Client-side document extraction (pdf.js, mammoth, jszip) with scanned-PDF
page rendering for the vision model; notes stay in their original language.
- ChatGPT-style shell: collapsible sidebar / mobile drawer, neutral token-based
light + dark themes, folder organisation, PWA (manifest, SW, offline page).
- Strict JSON validation with zod + auto-repair, rate limiting, and request
guards (cross-origin, body-size, API-key sanitising).
- setup.sh bootstraps deps, .env.local and a production build.
Gitignore excludes .env*, the generated pdf.js worker and local scratch files.