UI bug from a phone screenshot (short answer, Czech):
- The textarea was flex-shrunk inside the slide's column (58px instead of
~82px), clipping the typed answer mid-line. Every direct child of a
study/review/guide slide is now shrink-0 and the slide scrolls instead.
- Two AI disclaimers were visible at once: the activity's own after the
grading box plus the feed's row. The activity-level copy is gone; the
feed/guide row (which also carries the report control) is the single one.
The phase-2 check now asserts the note sits below the graded feedback.
- The mobile suite's backdrop-tap check is deterministic too (the spring
animation made the measured box mid-flight).
Ordering activities: the deck prompt now allows them only when the notes
hold a real sequence (chronological events, process stages, procedure steps,
a lifecycle, a ranked scale) and points at matching / multiple choice /
true-false when they do not - "sort these" was landing on decks with no
order at all.
Version 0.2.4, SW -> freezmo-v57.
- axe is clean across every screen in both themes, phone and desktop. Fixed:
the hidden file inputs (aria-hidden + tabIndex -1 — they are opened by
labelled buttons, so they should not be in the accessibility tree), the
heading order (deck/homework/smart-review cards are h2 under the page h1;
LearningSection takes a level so it stays h2 on the library and h3 inside
the settings dialog) and landmarks around the study and review feeds.
- tests/contrast.mjs (new diagnostic): samples rendered pixels, because axe
cannot judge text on blurred glass — it reports those nodes as incomplete.
It skips disabled controls (1.4.3 exempts them) and content behind an open
sheet. Result: no below-minimum text on any scanned screen.
- The report control exists in the homework guide too: under each "Try it
yourself" exercise, labelled "Report exercise", without a skip (there is no
queue to skip). lib/question-reports now takes deckId/deckTitle directly.
- The rail's two creation actions are buttons now (.nav-action) rather than
quiet nav rows — once the library is full they were the only way to start
something on desktop and read as just more navigation. "Make my homework"
is "New homework" wherever it is an action (rail, empty state, share
chooser, manifest shortcut); the page title and mode switch keep their
wording.
Version 0.1.6 (three task bumps), SW -> freezmo-v52.
UI batch from feedback:
- The study/review/exam/homework chrome is now a single 58px pill: round
close button, title + position over the progress bar, chips (or the exam
timer) at the far end — everything on the centre line, so nothing crowds
the rounded corners any more. Homework guide gets the same recipe.
- The immersive feeds carry z-[45] so they paint over the app chrome (on
/exam the header used to cover the top of the chrome), and the page chrome
behind an open overlay (.app-header, .segmented, .tabbar) is hidden while
the transparent overlay is up, so it can no longer ghost through the glass.
The guide's disclaimer moved inside the overlay with it.
- Chat: the context and model chips toggle their own popover (pressed state,
aria-expanded), opening one closes the other, a press outside closes it —
no more being forced to pick something to get out.
- Homework: the model's plain-text step explanations/hints/verdicts keep
their line breaks (whitespace-pre-wrap) — a step used to collapse into one
run-on paragraph with the whole solution inside it; the guide prompt now
says the explanation is coaching, 2-4 sentences.
- The four glacier blobs are at 25% of their old opacity (0.045 light,
0.1 dark).
Tests: phase-2 chrome geometry + exam layering checks (62), chat desktop
picker checks (101), the standing suites all green. README: decisions 54-56,
decision 23 updated. SW -> freezmo-v44.
The red "!" left the top chrome (it read as chrome and was easy to
mis-tap) and now sits in a row under the exercise: the AI disclaimer stays
centred, the report control is right-aligned with the card edge and is a
44px target. Applies to study, Smart Review and exam sessions; the results
slides keep just the disclaimer.
phase-2 suite now asserts the placement (below the card, right-aligned,
not in the header, >=44px) — 60 checks.
README: decision 52 updated. SW -> freezmo-v43.
The note now rides along with the content itself, not only with the
generation screens and the chat composer:
- Under every card in the study feed and in the review, mistakes and exam
sessions (the activity slide's last element — below the card, and below
the feedback panel once answered).
- On the results slides: the study results screen and the shared session
results (review, mistakes, exam).
Navigation surfaces (library, deck lists, chat history, settings) still
carry no note — it belongs to the moment of generation and to the card
being answered, not to browsing (README decision 50 updated).
The phase-2 grading check now scopes its placement (the disclaimer right
under the graded box) since the same text legitimately appears several
times on a session page; three new checks cover the feed, the results
slide and the exam (59 checks). SW -> freezmo-v40.
A generated matching card arrived on a phone with three identical
definitions (the generation guards now drop those), but there was no way
to get past a bad card from inside a session. The feed chrome (deck
study, review, mistakes and exam) now has the red "!" control: one tap
records the question, skips to the next card and shows an Undo toast.
- lib/question-reports.ts: the local list (freezmo.reports.v1, zod on
read, capped 200) is the skip state — reported cards leave every
session type, count as "not attempted" in the results instead of
wrong, and Undo takes a report back. A deck where everything is
reported stays studiable so it can be regenerated.
- POST /api/reports appends one JSON line per report to
REPORTS_DIR/reports.jsonl (shared/freezmo/reports in production, the
dev data dir in dev). Deliberately write-only: the question text is
readable on the server (that is the point — prompt tuning) but never
over HTTP, since the app has no auth. Best effort: offline reports stay
local.
- The button: a red outlined rounded square with "!", the chrome's
control size, in both feed headers (hidden on the results slide).
- config: REPORTS_DIR + RATE_LIMIT_REPORTS (20); the deploy creates the
shared reports dir; the dev unit and AGENTS.md note the new server
data location.
- Suites: phase-1 (append + validation + no GET), phase-2 (button, toast,
server POST, local copy, skip counter, hidden next session, Undo, exam
pool). SW -> freezmo-v38.
Answer history and schedules
- Every answer is recorded (deck, activity, topic, correct/wrong, time)
in a versioned IndexedDB log (freezmo-learning v1, stores answers +
srs), mirrored in memory for synchronous React reads and capped at
20k answers (oldest pruned). StudyFeed records as it grades.
- Light SM-2 schedule per activity: 1 day, 3 days, then interval x
ease; wrong answers return in ten minutes with the ease nudged down.
Review, mistakes, stats
- Library 'Smart review' card: due count, mistakes count (latest
answer wrong — clears itself when answered correctly), today's goal
progress. Review sessions freeze their item list at start.
- New /review (due across decks, ?mistakes=1 global or ?deck= per
deck) built on a shared SessionFeed with the study feed's snap/keyboard
mechanics, wake lock and a results slide.
- New /stats: day streak, five-week calendar, accuracy per subject and
per deck, weakest topics (>=3 answers), daily-goal progress.
- Daily goal configurable in Settings (10/20/30/50).
Exam mode
- New /exam: pick decks, question count, optional timer; no hints
(a NoHints context hides hint rows and the self-check escape);
results with score, per-deck and per-topic breakdown; wrong answers
land in the mistakes set through the normal history.
Reminders (conditional)
- VAPID-gated: GET/POST/DELETE /api/push and POST /api/push/test with
the standard route pattern, RATE_LIMIT_PUSH, zod, auth before body.
Subscriptions stored server-side as endpoint + keys only (PUSH_DIR).
UI hidden entirely without keys; iOS install note; service worker
push/notificationclick handlers; scripts/send-reminders.mjs for a
cron/systemd timer; web-push added as the one server dependency.
Verified: new phase-2 suite 42/42 (history/SRS shapes from a real study
run, mistakes clearing, review session, stats math, exam timer and
breakdowns, VAPID-less hidden state plus the configured
subscribe/validate/test/delete path against a second instance) plus
regression 74/74, mobile 52/52, wizard 80/80, declutter 116/116,
animation 8/8, native 35/35, phase1 38/38, i18n 25/25. Key parity
709/709. Lint + build clean. SW freezmo-v26.
Mobile only (<=639px / coarse pointers); desktop keeps its behaviour.
System integration
- Manifest: id, display_override, monochrome icon, Make my homework
shortcut and a share target (text/URL via GET, files via POST).
iOS startup images for the common iPhone sizes. Pinch-zoom is allowed
again (double-tap zoom is disabled via touch-action instead).
- Share target: /share chooser with New deck / Make my homework; POSTed
files wait in a 10-minute in-memory store, reach the wizard through
sessionStorage and never touch disk.
- Web Share: Share rows for decks (JSON file where supported) and the
homework result, with a clipboard fallback and a toast.
- Offline: a quiet pill appears and Freeze / mode cards are disabled
with a clear reason; input is never lost (error cards keep Retry).
- State: wizard drafts (text, options, photos up to a size budget)
survive reloads and OS kills and are cleared on success; re-tapping
the active tab scrolls to the top; the deck page shows a skeleton
instead of a spinner.
Navigation and gestures
- System Back closes the topmost layer first: a small history-entry
stack (lib/use-back-close.ts) covers the drawer, Settings sheet,
install sheet, deck actions panel and one wizard step; navigations
are never rewound and stale entries are skipped.
- Destructive-but-reversible actions use Undo toasts: deleting a deck
(no confirm) and removing a file in the wizard. Irreversible ones
keep their confirm.
Input and feedback
- Every phone input got inputmode / enterkeyhint / autocomplete /
autocapitalize / spellcheck; Enter on the last field advances the
wizard; a clipboard-paste button appears where the API exists.
- Haptics behind a Settings toggle (on by default): tab switches,
toggles, correct/wrong, completion; Vibration API + the iOS hidden
switch tick, throttled and silent everywhere else.
- Wake lock during the study feed and homework guide, released on
leave and re-acquired when the tab returns.
Verified: new native suite 35/35 (manifest/head/viewport, offline,
share target GET + POST, back-gesture layers, undo, share fallback,
tab re-tap, haptics persistence, draft restore), regression 74/74,
mobile 52/52, wizard 80/80, declutter 116/116, animation 8/8,
i18n 25/25. Lint + build clean. SW freezmo-v23.
Needs a real device (documented in the README): iOS haptic tick,
system share sheet entry, wake-lock grant, iPhone startup images,
iOS edge-swipe back, install prompts. Pull-to-refresh and the app
badge were skipped on purpose.
Mobile only (<=639px / coarse pointers); desktop keeps its behaviour.
Foundation
- lib/motion.ts owns the spring presets (snappy/gentle/sheet), durations,
easings and variants (screens, tabs, wizard steps, list stagger).
- Native-feel CSS: press feedback scales every button, card, chip, tab,
row and summary to 0.96 with a spring-like cubic-bezier on transform
(scoped to phones/coarse pointers), tap-highlight off, touch-action
manipulation, chrome text non-selectable, reduced motion drops it.
- App shell fades in once on launch; the study feed fades in on phones.
Applied
- Tab bar: sliding active pill (shared layoutId) + icon bounce.
- Study Decks/Homework, language toggle, difficulty and size pickers:
the same gliding highlight, gated so desktop keeps the old styling.
- Route content enters direction-aware (deeper = from the right, back =
from the left, same-depth = crossfade) via RouteTransition on the
library/create/homework content columns; library tab switches
crossfade; wizard steps slide forward/backward (reduced motion safe).
- Library: cards fade up in a capped stagger on first load, glide with
layout animations and shrink away on delete (AnimatePresence).
- Study feed: correct feedback pulses, wrong shakes, the progress bar
settles with a spring curve, score and XP count up on the results.
- Settings sheet uses the sheet spring; the drawer now follows the
finger and closes on release velocity, like the sheet.
- Generation progress: the stage message fades/slides between stages;
the icon keeps its gentle wobble; the feed fades in on hand-off.
Skipped, on purpose: LazyMotion (the sliding highlights need layout
animations = domMax, same features the current import already has);
route exit animations (the App Router unmounts the outgoing page —
exits are handled by in-page AnimatePresence); replacing the study
feed's native scroll-snap with finger-follow drag (would fight the
platform scroller); finger-follow height animation for native <details>
expanders.
Verified: animation suite 8/8 (reduced motion + 4x-CPU-throttled trace:
no long tasks, CLS 0.0000), regression 74/74, mobile 52/52, declutter
116/116, i18n 25/25. Lint + build clean. SW freezmo-v21.
Retrying the missed items could leave the feed on a stale position: the
retry swaps the queue (8 slides -> 3) and resets the scroll on the OLD
element, while the browser was still finishing a smooth scroll or snap.
Depending on the engine that could strand the viewport past the new
content (blank feed with the header still showing the old counter), and
the results slide kept its inner scroll because it has no key.
- The scroller now carries key={runId}, so every run mounts a brand-new
element: scroll position 0, fresh snap state, no inherited inner
scrolls in any slide.
- The scroll listener re-attaches to the new element (deps [runId]).
New retry suite 12/12: plain retry (fresh element, slide 1, header
Retry 1/N, missed item answerable again), retry fired mid smooth-scroll,
double-tap retry, and retry -> answer -> results -> retry again — all
land on slide 0 with content visible. Regression 74/74, mobile 51/51,
i18n 27/27. Lint + build clean. SW freezmo-v19.
English stays the default; Czech is a full translation. Only the app chrome
is translated - notes and generated decks keep their own language, which is a
core feature, not a setting.
i18n
- lib/i18n-dict.ts: flat en/cs dictionaries (441 keys each), translate() and
translatePlural() with real Czech plural rules (1 sada / 2-4 sady / 5+ sad).
English is the default and the fallback for a missing key.
- lib/i18n.ts: browser store (freezmo.locale.v1), useI18n() hook, and the
x-freezmo-locale header. The hook's t/plural are bound to the locale React
is rendering with, so the first client render matches the server HTML and
then re-renders in the stored language; module-level t is only for event
handlers and non-React helpers.
- <html lang> follows the choice (inline script + setLocale).
UI
- Settings -> Language segmented control (English / Cestina), plus an EN/CS
button on the key gate for first run.
- Every screen translated: sidebar, header, tab bar, library, deck cards,
folder bar, create flow, all seven activities, study chrome, results,
settings + health panel, API-key gate and help, install sheet, offline and
the deck-not-found page.
- Plural-aware counts everywhere (decks, folders, activities, runs, photos,
documents, pages, words, wrong attempts).
Server
- API routes and guards (generate, grade, health, http-guard, rate-limit,
ollama error mapping) answer in the request's language, so error cards and
the health panel never fall back to English mid-sentence.
Fixes
- A hydration mismatch when a non-default locale was stored silently dropped
the .dark class after hydration; render-time t() is now bound to the
hook's locale (UploadScreen.describeExtraction, ResultsScreen.answerFor,
DeckCard's folder fallback).
- Bumped the service worker cache to freezmo-v4 for this deploy.
Tests: i18n 27/27 (gate toggle, persistence, html lang, Czech plurals,
settings switch) and a rebuilt regression suite 72/72 (English default,
library, create, all 7 activity types, results, settings, install sheet,
offline, PWA, security, Czech smoke).
Rebuilds the look around the spec: an ice scene, one shared glass
recipe, icy accents and glass on every large surface. Visual only —
layout, behaviour, copy and functionality are untouched.
Background
- .ice-scene: fixed, z-index -1, pointer-events none. Navy base
(#070b16 to #0c1426) in dark, pale (#f4f9ff to #e6f1fb) in light,
four drifting glacier blobs (#38bdf8/#60a5fa/#818cf8, 30-60s
transform-only loops), five glossy ice orbs (40-220px radial
gradients with a highlight at 30% 25% and a blue shadow) placed so the
glass blurs them, and an SVG feTurbulence grain at 4-5%.
- prefers-reduced-motion stops the drift; phones get two blobs and two
orbs fewer.
Glass
- One shared recipe (.glass-panel, .card, .sidebar-surface,
.study-chrome, .tabbar, .glass, .gate-card): rgba(255,255,255,.06)
dark / .55 light, blur(24px) saturate(140%) (16px on phones), a bright
1px rim, an inset top highlight and a blue-tinted outer shadow.
- Nested glass (chips, inputs, secondary buttons) is more opaque and
never adds a second blur layer. @supports fallback for no
backdrop-filter. Radii: 24-28px cards/sheets, 14px inputs/buttons,
pills for chips. Cards hover brighter + cyan rim + 2px lift, 180ms.
Colour and type
- Accent is glacier cyan (#38bdf8) to ice blue (#0ea5e9); progress bars
and selected states use the gradient; the active sidebar item is a
frosted highlight with a cyan rim.
- Primary CTA: icy gradient — white to #bfe3ff with ink text in dark,
deep blue in light (biased so the label sits on the darker half and
keeps 5.6:1).
- Leftover slate/black greys replaced with navy-tinted frost tokens.
- Headings are semibold with tighter tracking.
Screens
- Dashboard: the sidebar is one tall inset rounded glass panel; deck
cards are glass.
- Gate: one centred glass card over the scene, orbs visible behind it.
- Sheets/modals: glass over the dimmed, lightly blurred backdrop.
- Study view: question card more opaque (.glass-solid); top bar and
pager are slim glass pills.
Perf: measured on a phone viewport with 4x CPU throttling. The animated
blobs' filter: blur() was the whole cost (150ms vs 33ms per frame), so
they are soft radial gradients with no filter now; idle 60fps, ~17ms
median while scrolling.
Tests updated for the new tokens: contrast 18/18 AA, deck chrome 15/15,
key gate 49/49, install sheet 64/64, deck fixes 15/15, retry 14/14,
zoom 6/6, final ux 12/12, security 8/8, pwa 16/16.
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.
The retry reused the same React keys for the same activity ids, so every
activity component kept its internal state (selected option, feedback,
flip, matched pairs) from the first run — the retried items appeared
pre-answered with the old wrong answer and could not be answered again.
- StudyFeed now keeps a runId that increments on every restart/retry and
includes it in the slide keys, so each run gets fresh component state.
- Scroll snaps back to the first slide instantly on a new run (the old
requestAnimationFrame + smooth goTo could race the queue change).
New retry suite (14 checks): answer wrong → results → retry → items are
unanswered and enabled → answer correctly → 2/2 on the retry run, plus the
full 'study again' reset. Deck-chrome 15/15, pwa 16/16, final-ux 12/12,
security 8/8, contrast 20/20.
- The study feed's progress header and bottom pager no longer use the old
navy glass: new .study-chrome class uses the neutral surface token
(same gray as the cards, #2f2f2f in dark), the neutral border
(8% white in dark, #e5e5e5 in light) and a neutral shadow.
- Close tile, streak pill and XP pill now use --surface-sunken /
--text-muted, matching the app's other buttons and chips; the close
glyph is the shared XIcon.
- Bottom pager: counter in --text, arrows in --text-muted.
- .glass (mobile header) no longer has the hardcoded #101720 navy; it is
now color-mix of the surface token. Removed the unused --color-night-*
tokens and the last night- usage (create screen sticky bar).
- Hidden the 'Swipe up' hint on desktop, where it collided with the
bottom pager.
New deck-chrome suite: 15/15 (computed surfaces/borders/text in both
themes, no navy); 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.