Phase 3: Check my work, follow-ups and turn-into-deck

Check my work (third homework mode)
- One model call with its own system prompt marks the student's own
  attempt: verdict, what's right, each issue with why + fix, and the
  corrected version that keeps everything the student got right.
  Unasked-for extras are never errors. The attempt is typed/pasted
  (photos of handwriting are documented as unsupported) and stored in
  the record so Redo re-checks the same work.
- Server: mode "check" + solution on POST /api/homework (zod, 400
  server.needSolution without one) and the same NDJSON stream; new
  checkHomework() pipeline with repair-once validation.

Follow-ups
- New POST /api/homework/refine (plain JSON, RATE_LIMIT_REFINE, zod,
  auth before body): shorter / more detail / explain, plain text back,
  language preserved.
- Shorter + More detail + Original rewrite the finished answer and the
  corrected version in place (FollowUpBar); every guide step has
  Explain step N differently; failed calls say so without losing text.

Turn into a study deck
- Any result hands its content (answer, corrected version or assembled
  guide) to New deck through a one-shot sessionStorage seed with the
  options step pre-filled.

UI
- Phone step 2 gains the third card: tapping it reveals the solution
  field + submit; desktop has three cards and the solution field shows
  with the mode. Homework history labels and share/card previews cover
  the new mode.

Verified: new phase-3 suite 20/20 against a stub Ollama (check payload,
missing-solution 400, refine validation, phone + desktop check flows,
shorter follow-up with restore, guide explain, deck hand-off) plus
regression 74/74, mobile 52/52, declutter 116/116, wizard 80/80,
animation 8/8, native 35/35, phase1 38/38, phase2 43/43, i18n 25/25.
Key parity 734/734. Lint + build clean. SW freezmo-v27.
This commit is contained in:
Martin Dvořák 2026-10-10 14:36:04 +00:00
commit 4513364019
22 changed files with 947 additions and 56 deletions

View file

@ -9,13 +9,17 @@ import { needsLanguageRepair } from "./language";
import {
buildGenerationUserPrompt,
buildGradingUserPrompt,
buildHomeworkCheckUserPrompt,
buildHomeworkGuideUserPrompt,
buildHomeworkMakeUserPrompt,
buildRefineUserPrompt,
buildRepairPrompt,
GENERATION_SYSTEM_PROMPT,
GRADING_SYSTEM_PROMPT,
HOMEWORK_CHECK_SYSTEM_PROMPT,
HOMEWORK_GUIDE_SYSTEM_PROMPT,
HOMEWORK_MAKE_SYSTEM_PROMPT,
REFINE_SYSTEM_PROMPT,
VISION_SYSTEM_PROMPT,
VISION_USER_INSTRUCTION,
} from "./prompts";
@ -23,12 +27,14 @@ import {
extractedNotesSchema,
generatedActivitySchema,
gradeResultSchema,
homeworkCheckSchema,
homeworkMakeSchema,
homeworkStepSchema,
type Difficulty,
type ExtractedNotes,
type GeneratedActivity,
type GradeResult,
type HomeworkCheck,
type HomeworkGuide,
type HomeworkMake,
} from "./schemas";
@ -502,6 +508,74 @@ export async function buildHomeworkGuide(input: {
return value;
}
/* ------------------------------------------------------------------ */
/* Check my work */
/* ------------------------------------------------------------------ */
const HOMEWORK_CHECK_SHAPE_HINT = `{"title":"...","language":"English name of the language used","verdict":"1-2 sentences","correct":["what is right"],"wrong":[{"issue":"...","why":"...","fix":"..."}],"corrected":"the student's solution with every mistake fixed"}`;
/** Mode 3: mark the student's own attempt and hand back a corrected version. */
export async function checkHomework(input: {
model: string;
auth: OllamaAuth;
assignment: string;
solution: string;
language?: string;
signal?: AbortSignal;
}): Promise<HomeworkCheck> {
const { value } = await runJsonStep<HomeworkCheck>({
model: input.model,
auth: input.auth,
system: HOMEWORK_CHECK_SYSTEM_PROMPT,
user: buildHomeworkCheckUserPrompt({
assignment: input.assignment,
solution: input.solution,
language: input.language,
}),
validate: (parsed) => {
const result = homeworkCheckSchema.safeParse(parsed);
if (!result.success) return { ok: false, error: summariseZodError(result.error) };
if (!result.data.corrected.trim()) return { ok: false, error: "The corrected field is empty." };
return { ok: true, value: result.data };
},
shapeHint: HOMEWORK_CHECK_SHAPE_HINT,
label: "Checking the attempt",
signal: input.signal,
temperature: 0.2,
});
return value;
}
/** Follow-ups: rewrite one finished piece (shorter / more detail / explain). */
export async function refineHomeworkText(input: {
model: string;
auth: OllamaAuth;
instruction: "shorter" | "more" | "explain";
text: string;
context?: string;
signal?: AbortSignal;
}): Promise<string> {
const result = await ollamaChat({
model: input.model,
auth: input.auth,
json: false,
signal: input.signal,
temperature: 0.4,
messages: [
{ role: "system", content: REFINE_SYSTEM_PROMPT },
{
role: "user",
content: buildRefineUserPrompt({
instruction: input.instruction,
text: input.text,
context: input.context,
}),
},
],
});
return result.content.trim();
}
/* ------------------------------------------------------------------ */
/* Helpers */
/* ------------------------------------------------------------------ */