Add “Make my homework”, and finish the mobile polish pass

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.
This commit is contained in:
Martin Dvořák 2026-10-08 18:30:33 +00:00
commit ef2a6a3589
35 changed files with 2334 additions and 100 deletions

View file

@ -167,6 +167,63 @@ const en: Record<string, string> = {
"health.createKey.after": "and paste it above. Tip: with the local daemon you can run",
"health.createKey.end": "instead and set",
/* make my homework ------------------------------------------------- */
"homework.nav": "Make my homework",
"homework.title": "Make my homework",
"homework.subtitle":
"Paste the assignment, snap a photo or add a document — then choose whether I should just do it, or guide you through it.",
"homework.entry": "Got homework instead of notes?",
"homework.source.title": "1. The assignment",
"homework.source.help": "Paste its text, drop photos, or add a document.",
"homework.source.textLabel": "Assignment text",
"homework.source.placeholder": "Paste the assignment here…",
"homework.source.add": "Add photos or files",
"homework.source.drop": "Drop the assignment here",
"homework.source.formats": "Text, photos, PDF, DOCX, PPTX, ODT/ODP, TXT, MD",
"homework.source.ready": "Ready when you are.",
"homework.source.need": "Add the assignment first.",
"homework.source.tooManyPhotos": "Up to {n} photos per assignment.",
"homework.source.tooManyDocs": "Up to {n} documents per assignment.",
"homework.mode.title": "2. What should I do?",
"homework.mode.make.title": "Just make it",
"homework.mode.make.subtitle": "The finished homework, ready to copy.",
"homework.mode.guide.title": "Explain how to make this homework",
"homework.mode.guide.subtitle": "A step-by-step guide with hints and quick checks.",
"homework.start.make": "Make my homework",
"homework.start.guide": "Guide me through it",
"homework.loading.title": "Working on the assignment…",
"homework.loading.reading": "Reading the assignment…",
"homework.loading.vision2": "Transcribing the questions…",
"homework.loading.writing": "Writing your homework…",
"homework.loading.planning": "Planning the steps…",
"homework.loading.gen2": "Checking every part…",
"homework.loading.gen3": "Almost done…",
"homework.error.title": "That didn't work",
"homework.error.body": "Something went wrong while working on the assignment.",
"homework.result.title": "Your homework",
"homework.result.copy": "Copy",
"homework.result.copied": "Copied!",
"homework.result.copyFailed": "Copy didn't work — select the text and copy it manually.",
"homework.result.again": "Make it again",
"homework.result.new": "New assignment",
"homework.guide.stepOf": "Step {n} of {total}",
"homework.guide.close": "Close the guide",
"homework.guide.tryIt": "Try it yourself",
"homework.guide.hint": "Show a hint",
"homework.guide.hintMore": "Another hint",
"homework.guide.hintAnswer": "Show the answer",
"homework.guide.hintN": "Hint {n}",
"homework.guide.next": "Next step",
"homework.guide.showSolution": "Show the solution",
"homework.guide.solution": "The full solution",
"homework.guide.solutionNote":
"Assembled from the steps above — copy it and hand it in.",
"homework.history.title": "Recent homework",
"homework.history.made": "Made",
"homework.history.guide": "Guide",
"homework.history.openTitle": "Open “{title}”",
"homework.history.deleteConfirm": "Delete this result?",
/* library --------------------------------------------------------- */
"library.title": "Your icebox",
"library.subtitle": "Decks frozen from your notes, saved in this browser.",
@ -534,6 +591,8 @@ const en: Record<string, string> = {
"server.bodyNotJson": "Request body was not valid JSON.",
"server.invalidRequest": "Invalid request: {detail}",
"server.needContent": "Add at least one photo of your notes or paste some text to build a deck.",
"server.needAssignment":
"Add the assignment first — paste its text, photograph it, or add a document.",
"server.invalidGrade": "Invalid grading request.",
"server.bodyTooLarge": "Request body is too large ({size}). Limit is {limit}.",
"server.invalidOrigin": "Invalid Origin header.",
@ -688,6 +747,63 @@ const cs: Record<string, string> = {
"health.createKey.after": "a vlož ho výše. Tip: s místním daemonem můžeš spustit",
"health.createKey.end": "a nastavit",
/* make my homework ------------------------------------------------- */
"homework.nav": "Udělej mi úkol",
"homework.title": "Udělej mi úkol",
"homework.subtitle":
"Vlož zadání, vyfoť ho nebo přidej dokument — a pak si vyber, jestli to mám rovnou udělat, nebo tě tím provést.",
"homework.entry": "Máš místo poznámek úkol?",
"homework.source.title": "1. Zadání",
"homework.source.help": "Vlož jeho text, přetáhni fotky nebo přidej dokument.",
"homework.source.textLabel": "Text zadání",
"homework.source.placeholder": "Sem vlož zadání…",
"homework.source.add": "Přidat fotky nebo soubory",
"homework.source.drop": "Přetáhni zadání sem",
"homework.source.formats": "Text, fotky, PDF, DOCX, PPTX, ODT/ODP, TXT, MD",
"homework.source.ready": "Můžeme začít.",
"homework.source.need": "Nejdřív přidej zadání.",
"homework.source.tooManyPhotos": "Až {n} fotek na zadání.",
"homework.source.tooManyDocs": "Až {n} dokumentů na zadání.",
"homework.mode.title": "2. Co mám udělat?",
"homework.mode.make.title": "Jen to udělej",
"homework.mode.make.subtitle": "Hotový úkol, připravený ke zkopírování.",
"homework.mode.guide.title": "Vysvětli, jak to udělat",
"homework.mode.guide.subtitle": "Návod krok za krokem s nápovědami a rychlými kontrolami.",
"homework.start.make": "Udělej mi úkol",
"homework.start.guide": "Proveď mě tím",
"homework.loading.title": "Pracuji na zadání…",
"homework.loading.reading": "Čtu zadání…",
"homework.loading.vision2": "Přepisuji otázky…",
"homework.loading.writing": "Píšu tvůj úkol…",
"homework.loading.planning": "Plánuji postup…",
"homework.loading.gen2": "Kontroluji každou část…",
"homework.loading.gen3": "Už to bude…",
"homework.error.title": "To se nepovedlo",
"homework.error.body": "Při zpracování zadání se něco pokazilo.",
"homework.result.title": "Tvůj úkol",
"homework.result.copy": "Kopírovat",
"homework.result.copied": "Zkopírováno!",
"homework.result.copyFailed": "Kopírování se nepovedlo — vyber text a zkopíruj ho ručně.",
"homework.result.again": "Udělej to znovu",
"homework.result.new": "Nové zadání",
"homework.guide.stepOf": "Krok {n}/{total}",
"homework.guide.close": "Zavřít návod",
"homework.guide.tryIt": "Zkus si to",
"homework.guide.hint": "Ukaž nápovědu",
"homework.guide.hintMore": "Další nápověda",
"homework.guide.hintAnswer": "Ukaž odpověď",
"homework.guide.hintN": "Nápověda {n}",
"homework.guide.next": "Další krok",
"homework.guide.showSolution": "Ukaž řešení",
"homework.guide.solution": "Celé řešení",
"homework.guide.solutionNote":
"Složeno ze všech kroků — zkopíruj a odevzdej.",
"homework.history.title": "Nedávné úkoly",
"homework.history.made": "Hotové",
"homework.history.guide": "Návod",
"homework.history.openTitle": "Otevřít „{title}“",
"homework.history.deleteConfirm": "Smazat tento výsledek?",
/* library --------------------------------------------------------- */
"library.title": "Tvoje mraznička",
"library.subtitle": "Sady zmrazené z tvých poznámek, uložené v tomto prohlížeči.",
@ -1055,6 +1171,8 @@ const cs: Record<string, string> = {
"server.bodyNotJson": "Tělo požadavku nebylo platné JSON.",
"server.invalidRequest": "Neplatný požadavek: {detail}",
"server.needContent": "Přidej aspoň jednu fotku poznámek nebo vlož text, ze kterého sada vznikne.",
"server.needAssignment":
"Nejdřív přidej zadání — vlož jeho text, vyfoť ho nebo přidej dokument.",
"server.invalidGrade": "Neplatný požadavek na známkování.",
"server.bodyTooLarge": "Tělo požadavku je příliš velké ({size}). Limit je {limit}.",
"server.invalidOrigin": "Neplatná hlavička Origin.",