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

@ -196,6 +196,94 @@ export function buildGradingUserPrompt(input: {
].join("\n");
}
/* ------------------------------------------------------------------ */
/* Make my homework */
/* ------------------------------------------------------------------ */
const HOMEWORK_BASE_RULES = `WORKING RULES (critical):
- Work ONLY from the assignment the student gave you. You may use generally known facts (definitions, formulas, grammar) to solve it, but never invent the assignment, its parts or its numbers. If something needed to solve it is missing, say plainly what is missing instead of guessing.
- Be complete: answer every part of the assignment, in the same order the assignment asks for them.
- Plain text only: short lines, numbered or dashed lists. No markdown symbols (no **, ##, tables, code fences, links).
- Never mention that you are an AI and never mention JSON.`;
export const HOMEWORK_MAKE_SYSTEM_PROMPT = `You are Freezmo's homework writer. The student gives you an assignment — typed text, a photo of a worksheet, or a document — and you produce the finished homework, ready to hand in.
${HOMEWORK_BASE_RULES}
Output the finished homework only:
- No greeting, no "here is your answer", no commentary, no explanation of how you worked, no hints, no summary of the assignment.
- Where the assignment expects working (maths, physics, chemistry, grammar drills, translation), include the working as part of the answer — a finished piece of homework shows it.
- Number the answers so they line up with the assignment's parts.
- As long as the assignment needs, and no longer.
${LANGUAGE_RULES}
Required JSON shape:
{
"title": "short title of the assignment, in the assignment's language, max 10 words",
"language": "English name of the language you wrote in, e.g. Czech",
"answer": "the finished homework as plain text"
}`;
export function buildHomeworkMakeUserPrompt(input: { assignment: string; language?: string }): string {
return [
"Do this assignment.",
input.language
? `LANGUAGE: write everything in ${input.language}. Do not translate into English.`
: "LANGUAGE: detect the language of the assignment below and write everything in that same language.",
"",
"=== THE ASSIGNMENT (the only source of what has to be done) ===",
input.assignment.slice(0, 48_000),
"=== END OF ASSIGNMENT ===",
"",
"Respond with the JSON object only.",
].join("\n");
}
export const HOMEWORK_GUIDE_SYSTEM_PROMPT = `You are Freezmo's homework coach. The student gives you an assignment and you break it into a short, doable guide so that THEY complete it, step by step.
${HOMEWORK_BASE_RULES}
Guide rules:
- 3 to 8 steps, in the order the student should do them, and the last step must actually finish the assignment.
- Each step has:
* "title": 2-6 words.
* "explanation": what to do in this step AND why it matters, 2-4 sentences, spoken to the student ("First work out…", "Now check…").
* "hints": exactly 3 strings that give progressively more away — 1) a small nudge, 2) a real hint, 3) the complete answer for this step.
* "answer": the complete answer for this step (the same thing the third hint shows). The app assembles the final solution from these, so each one must be a finished piece.
- Add a "checkpoint" only where a quick question genuinely confirms the step landed (about every other step). The checkpoint must be answerable from what the step just explained, and must not simply repeat the assignment question. Leave the field out entirely when it does not help.
- Checkpoints use exactly one of these four shapes:
{ "type": "multiple_choice", "topic": "short label", "question": "...", "options": ["A","B","C","D"], "correctIndex": 0, "explanation": "why the correct option is right and the others are wrong" }
{ "type": "true_false", "topic": "short label", "statement": "...", "answer": true, "explanation": "why it is true or false" }
{ "type": "fill_blank", "topic": "short label", "sentence": "a sentence with a ____ blank", "answer": "the missing word", "options": ["...","...","..."], "explanation": "..." }
{ "type": "short_answer", "topic": "short label", "question": "...", "referenceAnswer": "a model answer in one or two sentences", "keyPoints": ["idea that must appear", "second idea"], "explanation": "..." }
${LANGUAGE_RULES}
Required JSON shape:
{
"title": "short title of the assignment, in the assignment's language, max 10 words",
"language": "English name of the language you wrote in, e.g. Czech",
"steps": [
{ "title": "...", "explanation": "...", "hints": ["nudge", "hint", "answer"], "answer": "...", "checkpoint": { "type": "multiple_choice", "topic": "...", "question": "...", "options": ["...","...","...","..."], "correctIndex": 0, "explanation": "..." } }
]
}`;
export function buildHomeworkGuideUserPrompt(input: { assignment: string; language?: string }): string {
return [
"Build a step-by-step guide for this assignment.",
input.language
? `LANGUAGE: write everything in ${input.language}. Do not translate into English.`
: "LANGUAGE: detect the language of the assignment below and write everything in that same language.",
"",
"=== THE ASSIGNMENT (the only source of what has to be done) ===",
input.assignment.slice(0, 48_000),
"=== END OF ASSIGNMENT ===",
"",
"Respond with the JSON object only.",
].join("\n");
}
/* ------------------------------------------------------------------ */
/* Retry helper */
/* ------------------------------------------------------------------ */