Chat mode, phase 2: local history, answer actions, motion polish

- 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.
This commit is contained in:
Martin Dvořák 2026-10-11 19:03:48 +00:00
commit 5ca0de920c
19 changed files with 1107 additions and 113 deletions

View file

@ -711,6 +711,14 @@ have a step explained without leaving the app.
allowlist and never accepts a model name from the client.
- **Attachments.** Photos, PDFs and documents go through the existing vision/extraction step first
(plain text), so any chat model works — text-only included.
- **History.** Conversations live in a local IndexedDB store: a list on the left on desktop, a
sheet on phones, with auto titles from your first question, rename, delete (with Undo) and a
search across titles and message text. Conversations remember their scope and model, reopen from
`?c=<id>` (so a reload continues where you left off), stay readable offline, and are included in
the backup export and the encrypted sync — still never the key.
- **From an answer.** Every answer can become a deck (**Turn into flashcards** opens New deck with
the answer as notes, on the options step) or be appended to the scoped deck's notes (**Save to
notes**, only offered when exactly one deck is in scope).
- **Streaming and controls.** Tokens stream with a caret and a stop button; auto-scroll pauses when
you scroll up and a jump-to-bottom button appears; answers render safe markdown (lists, tables,
code blocks with copy — no HTML injection) and KaTeX loads dynamically only when a message
@ -1136,7 +1144,7 @@ Things I chose while building, with the reasoning:
limits, the learning engine, homework check/follow-ups). Suites that need a stub Ollama or
VAPID keys start their own `next start` instance on a spare port, verify the port is free and
kill the process group on exit; test scratch stays in `tests/.tmp/` (gitignored). Twelve suites,
487 checks.
513 checks.
46. **The ice orbs are rising bubbles now, composited and cheaper to read through.** The glossy
orbs (the glacier blobs stay exactly as they were) rise from below the bottom edge to above the
@ -1183,6 +1191,24 @@ Things I chose while building, with the reasoning:
points, streaming, stop, markdown + KaTeX, the model and scope pickers, edit/regenerate,
offline, attachments and reduced motion).
48. **Chat history is local, bounded and part of the backup/sync payload.** Conversations live in
their own versioned IndexedDB (`freezmo-chat` v1, store `chats`), validated with the same zod
schema the backup and sync use (`chatConversationSchema`), capped at 100 conversations × 300
messages, with the in-memory mirror the other stores use. Every settled turn is persisted (the
question always, the answer when one was produced — partial answers from a stop included); the
URL carries `?c=<id>` so a reload continues the conversation, and that URL tweak uses
`history.replaceState` so it never triggers a router round-trip mid-chat. The list (desktop
column, phone sheet) has auto titles from the first question, inline rename, delete with an Undo
toast, and diacritic-folding search over titles and message text. Chats ride along in the backup
file (the import preview counts them) and in the encrypted sync payload — still never the key.
Assistant answers also offer **Turn into flashcards** (opens New deck with the answer as notes,
landing on the options step) and **Save to notes** (appends to the single scoped deck, trimming
at 40k characters). Motion comes from `lib/motion.ts`: message fade-up, sheet springs, a 150 ms
composer height transition — all skipped under reduced motion. Verified by the chat suite (89
checks: everything from phase 1 plus history persistence, reload, rename/search/delete/undo,
both actions, the export payload, a two-device sync round-trip of a chat, offline readability
and the desktop panel).
36. **The mobile declutter pass reshapes, it doesn't fork.** Phones (≤639px) get a 3-step deck
flow and a 2-step homework flow that share the exact state and handlers with the desktop
cards (one `UploadScreen` / one `HomeworkInput`, two layouts switched by `useIsPhone()`);