The header and composer were full-bleed bands (flush to the top/right/bottom
viewport edges, composer corners off-screen) next to a rail that floats with a
12px margin. The new --layout-gutter (12px) spaces the rail, header, empty-state
card and composer identically on every side; --radius-panel/-control/-chip
(-pill) replace the ad-hoc radii across the shared classes, so every panel is
rounded on all four corners and shared controls match in every theme. The
desktop-only ::after hairlines and the left-corner-only radius are gone.
Aero: the rail and the chat chrome (answers included) share one even recipe -
the wallpaper extra-blurred into a hue-free wash (background-glass.webp) under a
uniform ~50% white veil and a whisper of gloss. Measured within ~10 RGB units
across the rail top->bottom and the card/composer left->right; the blue->green
smear and the teal composer are gone. The composer's field is a clean even tile
with no wallpaper gradient inside it.
Chat fixes: send is a filled primary tile shaped exactly like attach (dimmed
while the input is empty), suggestion/context chips use the shared .chip token
(40/44px target, visible rim, full-contrast text, raw model id in the tooltip),
the AI note reads at 12px --text-muted (.ai-disclaimer), the selected sidebar
row carries the accent border + accent icon, the theme toggle is back in Aero
(the moon flips to Dark; Settings brings Aero back), and the empty state is
centered exactly between header and composer.
Outside CSS: the wallpaper's top-left HUD-circle decoration (it peeked through
the 12px gutters on wide windows) was healed out of background.webp and
background-blur.webp; the orbs/bubbles ride one masked .ice-floats layer that
fades them under the chrome bands and the rail strip, so none is sliced by a
panel edge (still parked under reduced motion).
Version 0.6.0, SW -> freezmo-v82. Tests: chat suite pins the new geometry (12px
gutter, 24px corners, even wash on all four panels, send-vs-attach shape,
chips/input radii); mobile pins the phone chrome and the Aero toggle; bubbles
pins the floats layer + its reduced-motion opt-out.
1. Bugs fixed:
- Backup schema now accepts theme: "aero" so backups exported under
Frutiger Aero pass parseBackup on import, and applyImport in "replace"
mode replaces SRS schedules, answer history, and chats instead of
merging into existing SRS or skipping empty arrays.
- Typed FillBlank cards bind the input value to chosen ?? value instead
of chosen ?? "", and normalise() preserves Unicode letters (\p{L}\p{N})
when grading non-ASCII answers.
- OpenAI-compatible vision calls (contentFor in lib/openai-compat.ts)
prefix data:image/jpeg;base64, when routes pass stripped base64.
- Streaming calls (ollamaChatStream, providerChatStream) keep their
AbortController active across the entire body stream and API routes
forward request.signal so stopping a chat or cancelling a run aborts
the upstream request immediately.
- HomeworkCard uses homeworkModeLabelKey so "check" records display the
Checked label instead of Step-by-step guide.
- Desktop dropzones on /create and /homework accept dropped documents as
well as images.
- LibraryScreen filters reported questions out of its due and mistake
counts to match ReviewClient.
- Clearing a provider key resets generation if it pointed at that
provider; chat conversations persist and restore provider; ModelPicker
keys items by provider:name.
- RouteTransition no longer mutates module-level lastPath during render.
- Service worker asset handler falls back to Response.error() when an
uncached asset fetch fails offline.
- Confetti and .ice-orb / .ice-bubble respect the in-app reduce-motion
toggle.
2. Performance & cleanup:
- Deduplicate window storage listeners across all six localStorage
stores (one listener while subscribed instead of N^2 notifications).
- Use todayAnswerCount in LibraryScreen and pass sorted folders to
DeckCard.
- Memoize MessageRow with stable callbacks in ChatScreen and compute
last message IDs in a single pass.
- Close ImageBitmap after drawing in lib/image.ts and destroy pdf.js
documents in finally in lib/documents.ts.
- Avoid re-parsing serialized sync payloads in POST /api/sync.
- Deduplicate copyText (lib/share.ts -> lib/clipboard.ts), chatMessageId,
describeExtraction, ALL_FOLDERS, useIsPhone, and remove unused helpers
and boilerplate public/*.svg files.
Version 0.5.8, SW -> freezmo-v80.
The chat header and composer are full-bleed bands inside the main
pane, which starts right at the sidebar on desktop - so both ended in
a hard vertical edge beside the floating rail card (the 12px gutter
showed the raw scene and the bands' fill + backdrop blur read as a
color step at the seam), with their hairlines starting in mid-air at
that edge. Both bands now reach the card's right edge (the rail's own
12px padding, expanded or collapsed) and their hairlines are inset to
the content box; phones keep the plain full-width borders. The chat
suite pins the dock. The "Chat is set to ignore notes text" banner
went away - the scope chip already says what the chat may read.
Version 0.5.5, SW -> freezmo-v77. Chat 104/104 (2 new), mobile
126/126, wizard 95/95, phase3 31/31, bubbles 34/34, i18n 13/13,
native 33/33, regression 44/44, contrast 0 findings, axe clean.
Full e2e: 14/14 suites.
Buttons: the recipe's thin veils let btn-secondary/btn-ghost/glass-pill
blend into the grass and sky - they now carry a stronger pale-glass veil
(the Ask-pill look) over the blurred scene, ghosts plus a rim and
full-contrast text. Homework make/check results gained the same red "!"
report control as the feeds and the guide (a synthetic short-answer
activity maps into the report flow; pinned in phase3).
Reduced motion: the manual toggle only fed framer's MotionConfig, which
strips transform animations - while components branched on the OS-only
useReducedMotion - so translucent sheets still spring-faded their opacity
in over the page (the "ugly visual glitch with the settings panel"). All
16 components now read useAppReducedMotion() (OS or toggle), and the
settings sheet/scrim and the library's phone sheet go instant. Verified
frame by frame: opacity 1.00 from the first frame.
Contrast 0 findings, axe clean; mobile 126/126, chat 102/102, providers
20/20, wizard 95/95, phase3 31/31, bubbles 34/34, i18n 13/13, native
33/33. Version 0.5.4, SW -> freezmo-v76.
The welcome panel is the same frosted glass as the cards now (blurred,
not a flat tint) and stretches flush with the chat header and composer
(measured: 268..1280 at 1280, exactly the chrome's edges) - a flat pane
floating 40px inside the scene read as "cut off next to the sidebar".
Availability no longer over-promises: a /api/tags listing proves
availability only on a local daemon (the listing is the installed set);
on a cloud key the catalogue can include models a free plan cannot run,
and provider seeds before their first live /models test, so those stay
badge-less until actually used. The configured favourites keep their
verified state.
Contrast 0 findings, chat 102/102, providers 20/20, mobile 126/126,
i18n 13/13, native 33/33. Version 0.5.2, SW -> freezmo-v75.
The conversation column was max-w-3xl and centred, which on a 1280 window
left a ~140px empty strip between the rail and the messages - "the chat
cuts off weirdly". It now uses the library's container (max-w-6xl,
lg:px-10) and the composer's content padding matches, so the chat starts
right next to the sidebar and every row shares one left edge.
Contrast 0 findings, chat 102/102, mobile 126/126, i18n 13/13,
native 33/33. Version 0.5.0 (the 0.4.9 rollover), SW -> freezmo-v74.
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.
- Aero: blue gradients back on the active segment and every primary
(Review due, the card's Study button, the composer's send button); a
frosted backing behind the page heading and the All decks label; a
bottom-weighted rail tint; chat header/composer/welcome panels; calmer
orbs; stronger card veil + a stronger blur bake.
- Dark/Light: a dedicated --track token makes progress bars read (the
first attempt shifted --surface-sunken and the pixel scan caught the
sheet-contrast regression); brighter dark secondary text; darker light
segmented labels and difficulty tints; stronger light card border and
shadow.
- Chat UI: friendly model chip ("Gemma 4 31B") with the raw name in a
tooltip, larger chips, plain centered disclaimer, vertically centered
welcome panel, distinct chat-plus icon for new chat.
- Chat models: every chat-capable model from the server's /api/tags is
selectable (favourites first, search field when long, live-list
validation in the route); default CHAT_MODEL_DEFAULT or gemma4:31b.
- Reduce motion: Settings toggle (default off) that parks the scene,
collapses CSS animations and drives framer-motion's calm mode.
- Folders hint, "New" badge on fresh decks.
Contrast: 0 findings, axe clean, mobile 126/126, chat 102/102, wizard
95/95, regression 44/44, i18n 13/13, native 33/33.
Version 0.4.6, SW -> freezmo-v71.
Bubbles: ten -> eighteen (interleaved, 18px to 96px, 10-23s runs), with a
tighter specular hotspot, a reflected-light glint low on the sphere, a 1px
glass edge and a brighter inner rim on both the bubbles and the drifting
orbs. Bodies stay translucent with capped opacity, so wallpaper text stays
readable; bubble-perf shows no added frame cost.
Chat: the conversation scrolls up to the glass header, and a line clipped
exactly at that edge read as a rendering bug over the busy wallpaper - the
scroller now carries a 14px top mask in Aero, shorter than the column's
16px top padding so a fresh conversation shows no dimming.
Contrast 0 findings, axe clean, chat 101/101, mobile 123/123 (bubble count
pinned to 18), regression 44/44, i18n 13/13, native 33/33.
Version 0.3.7, SW -> freezmo-v66.
Two asks. "Slightly more transparent": the veils drop another notch
(recipe 0.15 -> 0.12, tokens ~0.03 less, tab bar / wizard bar / gloss
likewise). "Some buttons aren't themed": the recipe now catches the
flows' hand-rolled frost by attribute selectors - the study activities'
bg-white/xx chips (multiple choice, matching, ordering, fill blank),
the exam rows that used bg-[var(--surface)] (an opaque white in Aero),
and everything on the surface tokens (chat chips, homework chips,
inputs, dropzones). Inside sheets the same shapes stay flat; the
dropzones lost their press-scale (a transform re-anchors the fixed
backdrop) and shade inward instead.
And the chat: the assistant answers are bubble-less by design and the
user bubble's thin tint washed out over the bright wallpaper, so the
longest text in the app sat straight on the photo. In Aero both roles
get a real pane now - chat-answer wraps the markdown in a bubble, the
user turn carries chat-bubble, with a stronger veil than the shared
glass for comfortable reading. Light and dark keep the bubble-less
assistant design (styled only under .aero).
Contrast: 0 below-minimum findings, 0 axe violations. Chat 101/101,
mobile 123/123, regression 44/44, i18n 13/13, native 33/33.
Version 0.3.5, SW -> freezmo-v65.
Frutiger Aero (Settings -> Theme): a third theme, not a dark/light variant.
A sky gradient with a strip of grass, the glacier blobs become clouds, the
orbs soap bubbles, glass gets the classic horizon gloss and the buttons a
glossy aqua. The light/dark toggles (header, rail, chat header) hide while it
is on and the picker says why; the pre-paint script applies the class before
first paint and the PWA status bar follows (#3fa3e0). Its muted/frost text is
darker than Light's because those labels sit directly on the sky.
Contrast, measured on real pixels (tests/contrast.mjs, now covering aero):
- The light settings sheet was translucent over a dark scrim, so its muted
text measured ~2.8:1. Sheets and dialogs now use --glass-sheet-bg (a more
opaque surface), and Light's --text-muted is a touch darker.
- Result: 0 below-minimum findings and 0 axe violations across every screen,
all three themes, phone and desktop.
Also: the suites now seed the cookie notice as already dismissed (they run as
a returning user); the regression suite opts out to test the notice itself —
the previous deploy failed its gate because the notice blocked a chat chip
click. The notice also hides during immersive sessions (overlay-open), so it
can never cover a study feed.
Version 0.1.8, SW -> freezmo-v54.
- 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.
"Freezmo uses AI and can make mistakes. Double-check important info."
(plus a natural informal Czech translation), rendered by a shared
AiDisclaimer so the copy, style and translations live in one place.
Placements — only where the model's words reach the screen:
- Chat: under the composer, below the context/model chips — always (chat
is pure model output, so it also shows in the empty state).
- Make my homework: under the start CTA (desktop) and the mode cards'
helper line (phone), plus once under the generated result for all three
modes (make / check / guide).
- New deck wizard: under the summary helper (phone step 3) and the desktop
CTA, next to Freeze my deck.
- AI grading (short answers): under the graded feedback; study, review and
exam all share it.
Stored, already-generated content (deck page, study cards, explanations)
deliberately carries no note: the disclaimer belongs to the moment of
generation, not to browsing. Style: --text-subtle (documented at 5.6:1
light / 8.6:1 dark), 11.5px, centred, wraps to two lines at 360px, not
clickable, no icon.
Suites: chat (composer, phone + desktop), wizard (deck step 3 across
360/390 x en/cs x dark/light, homework start cards), phase3 (homework
trigger + result, phone + desktop), phase2 (stubbed grading feedback).
SW -> freezmo-v37. README: decision 50 + chat composer note.
Also fixes the deploy script's test instance: it now execs the server so
the cleanup kill reaps the real PID (the wrapper subshell left orphans
squatting on the test port — three from earlier sessions were found while
verifying this task), and the readiness poll checks the port serves the
NEW commit rather than just answering, so a leftover instance can never
be tested silently.
- Desktop: on /chat the main sidebar shows the chat list (search, New chat,
items with rename/delete, active highlight) in place of the deck
navigation; the separate second column is gone and the chat uses the full
width. The expand/collapse chevron sits outside the swappable section and
a collapsed rail stays clean (the highlighted Chat row already opens a
new chat). Active conversation travels via a tiny store (lib/chat-active)
so the sidebar needs no useSearchParams and pages stay static.
- Header (phones): history, theme and New chat are now identical chips -
same 44px hit area, rounded-2xl, border/surface tokens, size-5 icons,
even spacing; the title block truncates cleanly. ThemeToggle gained a
'plain' variant so the header styles it without touching other pages.
- ChatIcon redrawn (clean message-circle, no inner lines) and the empty
state uses the app's icon-chip pattern (size-12 chip, accent-600/200)
instead of the bare oversized glyph; the model chip icon follows.
- Also fixed a pre-existing dev-only breakage found while verifying: the
tab-bar bounce animated three keyframes with a spring, which framer
motion's dev build throws on - breaking client updates in the dev
instance (library tab switching, sheet Escape). It now uses a tween.
- Dev metadata keeps the Apple icons in both branches.
- SW -> freezmo-v35.
- Conversations in a versioned IndexedDB store (freezmo-chat v1, zod
validated with the same schema backup/sync use, capped at 100 x 300).
Settled turns persist (question always; answer when produced, partial
stops included); ?c=<id> keeps reloads on the conversation via
history.replaceState (no router round-trip mid-chat).
- History UI: desktop column, phone sheet (spring, backdrop, Escape),
auto titles from the first question, inline rename, delete with an
Undo toast, diacritic-folding search over titles and message text.
Scope and model are remembered per conversation; history is readable
offline.
- Assistant actions: Turn into flashcards (New deck with the answer as
notes, landing on the options step - deck seed gained step) and Save
to notes (appends to the single scoped deck, trimmed at 40k).
- Chats travel in the backup export (preview counts them) and in the
encrypted sync payload; still never the key.
- Motion from lib/motion.ts: message fade-up, sheet springs for the
model/context/history sheets, 150ms composer height transition, all
skipped under reduced motion.
- Tests: chat suite 63 -> 89 checks (history, reload, rename/search/
delete/undo, actions, export, two-device sync round-trip, offline,
desktop panel). Full set: 12/12 suites, 513 checks. SW -> freezmo-v33.
Also: tests/model-compare.mjs benchmark (diagnostic) for deciding the
default text model on numbers; defaults unchanged.
Entry points (no sixth tab): Ask in the Library header (desktop button,
phone icon), Ask about this deck in the deck menu, Ask a question on
study/homework results (compact result travels as context), and a Chat
row in the desktop sidebar. Phones get a full-screen route with back +
New chat; the tab bar steps aside.
- Client-side retrieval: chunk scoped decks (~800 tokens), BM25 against
the question + recent turns, top chunks under ~7k tokens, each
labelled with deck + section. Notes come from the deck's stored
sourceText (already kept for Regenerate); decks without it fall back
to generated content and the UI says so.
- Stateless server: /api/chat validates against the CHAT_MODELS
allowlist, trims the history to ~6k tokens, caps context at ~9k and
streams token/thinking/done/error NDJSON. /api/chat/extract reduces
attachments to text through the existing vision step.
- UI: user bubbles + full-width assistant text, streaming with stop,
auto-scroll that pauses with a jump-to-bottom button, safe markdown
(React nodes, no HTML injection; KaTeX dynamically imported only when
a message has math), Thinking row for reasoning, copy/regenerate/
edit-and-resend, model + context pickers (last model remembered),
composer with visualViewport keyboard handling, safe-area, 16px
textarea, Enter-to-send on desktop.
- Usage-limit failures keep the draft and show the shared message with
Settings/Ollama actions; stopping maps the reader abort to a clean
stopped state; offline disables sending with a reason.
- Settings: Keep notes text for chat toggle; deck menu: Delete notes
text. RATE_LIMIT_CHAT + CHAT_MODELS in .env.example and README.
- Tests: chat suite (63 checks, stub Ollama) wired into run-all.
Full set: 12/12 suites, 487 checks. SW -> freezmo-v32.
Also: tests/model-compare.mjs (diagnostic) benchmarks two models on the
same notes (latency, repair rounds, activity types) so the gpt-oss vs
gemma default can be decided on numbers; defaults unchanged.