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

@ -0,0 +1,76 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui";
import { refineHomework } from "@/lib/client-api";
import { useI18n } from "@/lib/i18n";
/**
* Follow-up actions for one finished piece of homework: shorter, more detail,
* or back to the original. Used on the make result and the corrected version
* of a check.
*/
export function FollowUpBar({
text,
context,
onReplace,
onRestore,
canRestore,
}: {
text: string;
context?: string;
onReplace: (next: string) => void;
onRestore?: () => void;
canRestore?: boolean;
}) {
const { t } = useI18n();
const [busy, setBusy] = useState<"shorter" | "more" | null>(null);
const [failed, setFailed] = useState(false);
const run = async (instruction: "shorter" | "more") => {
if (busy) return;
setBusy(instruction);
setFailed(false);
try {
const next = await refineHomework({ instruction, text, context });
onReplace(next);
} catch {
setFailed(true);
} finally {
setBusy(null);
}
};
return (
<div className="flex flex-wrap items-center gap-2">
<Button
size="sm"
variant="secondary"
className="touch-target"
loading={busy === "shorter"}
disabled={Boolean(busy)}
onClick={() => void run("shorter")}
>
{t("homework.followup.shorter")}
</Button>
<Button
size="sm"
variant="secondary"
className="touch-target"
loading={busy === "more"}
disabled={Boolean(busy)}
onClick={() => void run("more")}
>
{t("homework.followup.more")}
</Button>
{busy ? <span className="text-[12px] text-[var(--text-subtle)]">{t("homework.followup.working")}</span> : null}
{canRestore && onRestore ? (
<Button size="sm" variant="ghost" className="touch-target" disabled={Boolean(busy)} onClick={onRestore}>
{t("homework.followup.restore")}
</Button>
) : null}
{failed ? <span className="text-[12px] font-medium text-rose-600 dark:text-rose-300">{t("homework.followup.failed")}</span> : null}
</div>
);
}

View file

@ -0,0 +1,113 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui";
import { useI18n } from "@/lib/i18n";
import type { HomeworkCheckRecord } from "@/lib/schemas";
import { CopyButton } from "./CopyButton";
import { FollowUpBar } from "./FollowUpBar";
/** Mode 3 result: verdict, what's right, what needs fixing, corrected version. */
export function HomeworkCheckResult({
record,
onAgain,
onNew,
onToDeck,
againLoading,
}: {
record: HomeworkCheckRecord;
onAgain: () => void;
onNew: () => void;
onToDeck: () => void;
againLoading: boolean;
}) {
const { t } = useI18n();
const [corrected, setCorrected] = useState(record.corrected);
return (
<div className="space-y-5">
<header className="space-y-1">
<p className="text-[11px] font-semibold uppercase tracking-wider text-[var(--text-subtle)]">
{t("homework.check.title")}
</p>
<h1 className="text-[22px] font-semibold leading-tight tracking-[-0.02em] text-[var(--text)] text-balance">
{record.title}
</h1>
</header>
<section className="card space-y-1.5 p-4">
<p className="text-[11px] font-semibold uppercase tracking-wide text-[var(--text-subtle)]">
{t("homework.check.verdict")}
</p>
<p className="text-[15px] leading-relaxed text-[var(--text)]">{record.verdict}</p>
</section>
{record.correct.length > 0 ? (
<section className="rounded-2xl border border-emerald-200/80 bg-emerald-50/90 p-4 dark:border-emerald-400/30 dark:bg-emerald-500/10">
<p className="text-[11px] font-semibold uppercase tracking-wide text-emerald-800 dark:text-emerald-200">
{t("homework.check.correct")}
</p>
<ul className="mt-2 list-inside list-disc space-y-1 text-[14px] leading-relaxed text-emerald-900 dark:text-emerald-100">
{record.correct.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</section>
) : null}
{record.wrong.length > 0 ? (
<section className="rounded-2xl border border-rose-200/80 bg-rose-50/90 p-4 dark:border-rose-400/30 dark:bg-rose-500/10">
<p className="text-[11px] font-semibold uppercase tracking-wide text-rose-800 dark:text-rose-200">
{t("homework.check.wrong")}
</p>
<ul className="mt-2 space-y-3">
{record.wrong.map((entry, index) => (
<li key={index} className="space-y-0.5 text-[14px] leading-relaxed text-rose-900 dark:text-rose-100">
<p className="font-semibold">{entry.issue}</p>
<p>{entry.why}</p>
{entry.fix ? (
<p>
<span className="font-bold">{t("homework.check.fix")}: </span>
{entry.fix}
</p>
) : null}
</li>
))}
</ul>
</section>
) : null}
<article className="card space-y-3 p-4 sm:p-5">
<p className="text-[11px] font-semibold uppercase tracking-wide text-[var(--text-subtle)]">
{t("homework.check.corrected")}
</p>
<p className="whitespace-pre-wrap break-words text-[15px] leading-relaxed text-[var(--text)]">{corrected}</p>
<div className="flex flex-wrap items-center gap-2">
<CopyButton text={corrected} />
<FollowUpBar
text={corrected}
context={record.sourceText}
onReplace={setCorrected}
onRestore={() => setCorrected(record.corrected)}
canRestore={corrected !== record.corrected}
/>
</div>
</article>
<div className="flex flex-wrap items-start gap-2">
<Button variant="secondary" className="touch-target" onClick={onAgain} loading={againLoading}>
{t("homework.followup.redo")}
</Button>
<Button variant="secondary" className="touch-target" onClick={onToDeck}>
{t("homework.toDeck")}
</Button>
<Button variant="ghost" className="touch-target" onClick={onNew}>
{t("homework.result.new")}
</Button>
</div>
<p className="text-[11.5px] text-[var(--text-subtle)]">{t("homework.toDeckNote")}</p>
</div>
);
}

View file

@ -11,6 +11,7 @@ import { assembleSolution } from "@/lib/homework";
import { useI18n } from "@/lib/i18n";
import type { HomeworkGuideRecord, HomeworkStep } from "@/lib/schemas";
import { useWakeLock } from "@/lib/use-wake-lock";
import { refineHomework } from "@/lib/client-api";
import { CopyButton } from "./CopyButton";
@ -25,12 +26,14 @@ export function HomeworkGuide({
onClose,
onAgain,
onNew,
onToDeck,
againLoading,
}: {
record: HomeworkGuideRecord;
onClose: () => void;
onAgain: () => void;
onNew: () => void;
onToDeck: () => void;
againLoading: boolean;
}) {
const { t } = useI18n();
@ -40,8 +43,30 @@ export function HomeworkGuide({
const slideCount = steps.length + 1; // + the solution slide
const [index, setIndex] = useState(0);
const [revealed, setRevealed] = useState<Record<number, number>>({});
/** "Explain this step differently" overrides, per step index. */
const [explained, setExplained] = useState<Record<number, string>>({});
const [explaining, setExplaining] = useState<number | null>(null);
const [explainFailed, setExplainFailed] = useState<number | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const explainStep = async (stepIndex: number) => {
if (explaining !== null) return;
setExplaining(stepIndex);
setExplainFailed(null);
try {
const next = await refineHomework({
instruction: "explain",
text: steps[stepIndex].explanation,
context: record.sourceText,
});
setExplained((current) => ({ ...current, [stepIndex]: next }));
} catch {
setExplainFailed(stepIndex);
} finally {
setExplaining(null);
}
};
const goTo = useCallback(
(target: number) => {
const container = scrollRef.current;
@ -164,8 +189,25 @@ export function HomeworkGuide({
<PromptCard>
<QuestionText>{step.title}</QuestionText>
<p className="mt-2 text-[15px] leading-relaxed text-frost-700 dark:text-frost-200">
{step.explanation}
{explained[stepIndex] ?? step.explanation}
</p>
<div className="mt-2 flex flex-wrap items-center gap-2">
<Button
size="sm"
variant="ghost"
className="touch-target"
loading={explaining === stepIndex}
disabled={explaining !== null}
onClick={() => void explainStep(stepIndex)}
>
{t("homework.followup.step", { n: stepIndex + 1 })}
</Button>
{explainFailed === stepIndex ? (
<span className="text-[12px] font-medium text-rose-600 dark:text-rose-300">
{t("homework.followup.failed")}
</span>
) : null}
</div>
</PromptCard>
{/* progressive hints */}
@ -242,12 +284,16 @@ export function HomeworkGuide({
<div className="flex flex-wrap items-start gap-2">
<CopyButton text={solution} />
<Button variant="secondary" className="touch-target" onClick={onAgain} loading={againLoading}>
{t("homework.result.again")}
{t("homework.followup.redo")}
</Button>
<Button variant="secondary" className="touch-target" onClick={onToDeck}>
{t("homework.toDeck")}
</Button>
<Button variant="ghost" className="touch-target" onClick={onNew}>
{t("homework.result.new")}
</Button>
</div>
<p className="text-[11.5px] text-[var(--text-subtle)]">{t("homework.toDeckNote")}</p>
</div>
</section>
</div>

View file

@ -4,7 +4,7 @@ import { motion, useReducedMotion } from "framer-motion";
import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { CameraIcon, FolderIcon, PlayIcon, PlusIcon, SparklesIcon } from "@/components/icons";
import { CameraIcon, CheckCircleIcon, FolderIcon, PlayIcon, PlusIcon, SparklesIcon } from "@/components/icons";
import { Button } from "@/components/ui";
import { WizardActionBar } from "@/components/WizardActionBar";
import {
@ -47,6 +47,7 @@ function describeExtraction(
const MODES: Array<{ id: HomeworkMode; icon: React.ReactNode; titleKey: string; subtitleKey: string }> = [
{ id: "make", icon: <SparklesIcon className="size-5" />, titleKey: "homework.mode.make.title", subtitleKey: "homework.mode.make.subtitle" },
{ id: "guide", icon: <PlayIcon className="size-5" />, titleKey: "homework.mode.guide.title", subtitleKey: "homework.mode.guide.subtitle" },
{ id: "check", icon: <CheckCircleIcon className="size-5" />, titleKey: "homework.mode.check", subtitleKey: "homework.mode.checkDesc" },
];
export function HomeworkInput({
@ -54,6 +55,8 @@ export function HomeworkInput({
onModeChange,
text,
onTextChange,
solution,
onSolutionChange,
intake,
onSubmit,
error,
@ -65,6 +68,9 @@ export function HomeworkInput({
onModeChange: (mode: HomeworkMode) => void;
text: string;
onTextChange: (text: string) => void;
/** Mode "check": the student's own attempt. */
solution: string;
onSolutionChange: (solution: string) => void;
intake: ReturnType<typeof useNotesIntake>;
/** `modeOverride` lets the phone mode cards start the run they tapped. */
onSubmit: (modeOverride?: HomeworkMode) => void;
@ -80,6 +86,8 @@ export function HomeworkInput({
const [step, setStep] = useState(1);
const [stepDir, setStepDir] = useState<1 | -1>(1);
const [pasteOpen, setPasteOpen] = useState(false);
/** Phones: the check card reveals the solution field instead of starting. */
const [checkOpen, setCheckOpen] = useState(false);
const isPhone = useIsPhone();
const reduceMotion = useReducedMotion();
const online = useOnline();
@ -176,6 +184,29 @@ export function HomeworkInput({
</div>
) : null;
/* Shared solution field: typed/pasted only (documented in the note). */
const solutionField = (
<div className="space-y-1.5">
<label className="text-[12px] font-semibold text-[var(--text)]" htmlFor="homework-solution">
{t("homework.solution.label")}
</label>
<textarea
id="homework-solution"
value={solution}
onChange={(event) => onSolutionChange(event.target.value)}
rows={5}
placeholder={t("homework.solution.placeholder")}
inputMode="text"
enterKeyHint="done"
autoComplete="off"
autoCapitalize="sentences"
spellCheck
className="w-full resize-y rounded-2xl border border-white/60 bg-white/55 px-4 py-3 text-sm text-frost-950 outline-none backdrop-blur-xl transition placeholder:text-frost-400 focus:border-accent-400 dark:border-white/12 dark:bg-white/[0.07] dark:text-frost-50"
/>
<p className="text-[11px] leading-snug text-[var(--text-subtle)]">{t("homework.solution.note")}</p>
</div>
);
/* Shared mode card: desktop toggles the selection, the phone flow starts it. */
const modeCard = (option: (typeof MODES)[number], onPick: () => void) => {
const selected = mode === option.id;
@ -479,12 +510,35 @@ export function HomeworkInput({
{MODES.map((option) =>
modeCard(option, () => {
if (!canSubmit || !online) return;
if (option.id === "check") {
// Needs the solution first: reveal the field instead of
// starting the run.
onModeChange("check");
setCheckOpen(true);
return;
}
clearWizardDraft("homework");
onModeChange(option.id);
onSubmit(option.id);
}),
)}
</div>
{checkOpen ? (
<div className="space-y-3">
{solutionField}
<Button
size="lg"
className="touch-target w-full"
disabled={!canSubmit || !solution.trim()}
onClick={() => {
clearWizardDraft("homework");
onSubmit("check");
}}
>
{t("homework.mode.check")}
</Button>
</div>
) : null}
<p className="text-center text-[12px] text-[var(--text-subtle)]">
{online ? t("homework.phone.startHint") : t("offline.generate")}
</p>
@ -704,16 +758,23 @@ export function HomeworkInput({
{/* ------------------------------------------------------- mode */}
<section className="space-y-3">
<h2 className="text-[15px] font-semibold tracking-[-0.01em] text-[var(--text)]">{t("homework.mode.title")}</h2>
<div className="grid gap-3 sm:grid-cols-2">
<div className="grid gap-3 sm:grid-cols-3">
{MODES.map((option) => modeCard(option, () => onModeChange(option.id)))}
</div>
{mode === "check" ? solutionField : null}
</section>
{/* -------------------------------------------------------- CTA */}
<div className="space-y-2">
<Button size="lg" className="touch-target w-full" onClick={() => onSubmit()} disabled={!canSubmit} loading={intake.busy}>
<Button
size="lg"
className="touch-target w-full"
onClick={() => onSubmit()}
disabled={!canSubmit || (mode === "check" && !solution.trim())}
loading={intake.busy}
>
<SparklesIcon className="size-5" />
{mode === "make" ? t("homework.start.make") : t("homework.start.guide")}
{mode === "make" ? t("homework.start.make") : mode === "guide" ? t("homework.start.guide") : t("homework.mode.check")}
</Button>
<p className="text-center text-[12px] text-[var(--text-subtle)]">
{canSubmit ? t("homework.source.ready") : t("homework.source.need")}

View file

@ -1,25 +1,31 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui";
import { useI18n } from "@/lib/i18n";
import type { HomeworkMakeRecord } from "@/lib/schemas";
import { shareHomework } from "@/lib/share";
import { CopyButton } from "./CopyButton";
import { FollowUpBar } from "./FollowUpBar";
/** Mode 1 result: the finished homework, nothing else. */
export function HomeworkResult({
record,
onAgain,
onNew,
onToDeck,
againLoading,
}: {
record: HomeworkMakeRecord;
onAgain: () => void;
onNew: () => void;
onToDeck: () => void;
againLoading: boolean;
}) {
const { t } = useI18n();
const [answer, setAnswer] = useState(record.answer);
return (
<div className="space-y-5">
@ -32,24 +38,33 @@ export function HomeworkResult({
</h1>
</header>
<article className="card p-4 sm:p-5">
<p className="whitespace-pre-wrap break-words text-[15px] leading-relaxed text-[var(--text)]">
{record.answer}
</p>
<article className="card space-y-3 p-4 sm:p-5">
<p className="whitespace-pre-wrap break-words text-[15px] leading-relaxed text-[var(--text)]">{answer}</p>
<FollowUpBar
text={answer}
context={record.sourceText}
onReplace={setAnswer}
onRestore={() => setAnswer(record.answer)}
canRestore={answer !== record.answer}
/>
</article>
<div className="flex flex-wrap items-start gap-2">
<CopyButton text={record.answer} />
<Button variant="secondary" className="touch-target" onClick={() => void shareHomework(record)}>
<CopyButton text={answer} />
<Button variant="secondary" className="touch-target" onClick={() => void shareHomework({ ...record, answer })}>
{t("deck.share")}
</Button>
<Button variant="secondary" className="touch-target" onClick={onAgain} loading={againLoading}>
{t("homework.result.again")}
{t("homework.followup.redo")}
</Button>
<Button variant="secondary" className="touch-target" onClick={onToDeck}>
{t("homework.toDeck")}
</Button>
<Button variant="ghost" className="touch-target" onClick={onNew}>
{t("homework.result.new")}
</Button>
</div>
<p className="text-[11.5px] text-[var(--text-subtle)]">{t("homework.toDeckNote")}</p>
</div>
);
}

View file

@ -1,10 +1,13 @@
"use client";
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
import { useRouter } from "next/navigation";
import { documentsToText } from "@/lib/documents";
import { ApiError, generateHomework, type HomeworkProgress } from "@/lib/client-api";
import { setDeckSeed } from "@/lib/deck-seed";
import {
assembleSolution,
deleteHomework,
getHomework,
homeworkStore,
@ -18,6 +21,7 @@ import type { HomeworkMode, HomeworkRecord } from "@/lib/schemas";
import { useNotesIntake } from "@/lib/use-notes-intake";
import { clearWizardDraft } from "@/lib/wizard-draft";
import { HomeworkCheckResult } from "./HomeworkCheckResult";
import { HomeworkGuide } from "./HomeworkGuide";
import { HomeworkInput } from "./HomeworkInput";
import { HomeworkLoading } from "./HomeworkLoading";
@ -32,6 +36,7 @@ type View = { kind: "start" } | { kind: "loading" } | { kind: "record"; record:
/** The /homework screen: give it an assignment, pick a mode, get a result. */
export function HomeworkScreen({ initialRecordId }: { initialRecordId?: string }) {
const { t } = useI18n();
const router = useRouter();
const records = useSyncExternalStore(
homeworkStore.subscribe,
homeworkStore.getSnapshot,
@ -45,6 +50,8 @@ export function HomeworkScreen({ initialRecordId }: { initialRecordId?: string }
const [mode, setMode] = useState<HomeworkMode>("make");
const [text, setText] = useState("");
/** Mode "check": the student's own attempt at the assignment. */
const [solution, setSolution] = useState("");
const [view, setView] = useState<View>({ kind: "start" });
const [progress, setProgress] = useState<HomeworkProgress>({ stage: "vision", message: "" });
const [error, setError] = useState<{ message: string; kind?: string } | null>(null);
@ -68,11 +75,16 @@ export function HomeworkScreen({ initialRecordId }: { initialRecordId?: string }
...intake.images.map((image) => image.dataUrl),
...documentImages.map((image) => image.dataUrl),
].slice(0, MAX_IMAGES_TO_SEND);
return { mode: modeValue, images, text: mergedText };
return {
mode: modeValue,
images,
text: mergedText,
solution: modeValue === "check" ? solution.trim() : undefined,
};
};
const run = useCallback(
async (input: { mode: HomeworkMode; images: string[]; text: string; recordId?: string }) => {
async (input: { mode: HomeworkMode; images: string[]; text: string; solution?: string; recordId?: string }) => {
setError(null);
setView({ kind: "loading" });
const vision = input.images.length > 0;
@ -82,7 +94,9 @@ export function HomeworkScreen({ initialRecordId }: { initialRecordId?: string }
? t("homework.loading.reading")
: input.mode === "make"
? t("homework.loading.writing")
: t("homework.loading.planning"),
: input.mode === "check"
? t("homework.loading.checking")
: t("homework.loading.planning"),
});
const controller = new AbortController();
@ -94,12 +108,13 @@ export function HomeworkScreen({ initialRecordId }: { initialRecordId?: string }
mode: input.mode,
images: input.images.length > 0 ? input.images : undefined,
text: input.text || undefined,
solution: input.solution,
},
{ onProgress: setProgress, signal: controller.signal },
);
// The server hands back the assignment as text (the transcription when
// photos were used), so “Make it again” works without the files.
// photos were used), so “Redo” works without the files.
const sourceText = homework.sourceText ?? input.text;
const result: NewHomeworkResult =
homework.mode === "make"
@ -110,13 +125,25 @@ export function HomeworkScreen({ initialRecordId }: { initialRecordId?: string }
sourceText,
answer: homework.answer,
}
: {
mode: "guide",
title: homework.title,
language: homework.language,
sourceText,
steps: homework.steps,
};
: homework.mode === "check"
? {
mode: "check",
title: homework.title,
language: homework.language,
sourceText,
verdict: homework.verdict,
correct: homework.correct,
wrong: homework.wrong,
corrected: homework.corrected,
attempt: input.solution,
}
: {
mode: "guide",
title: homework.title,
language: homework.language,
sourceText,
steps: homework.steps,
};
const record = input.recordId ? updateHomework(input.recordId, result) : saveHomework(result);
if (!record) throw new ApiError(t("homework.error.body"), "bad_response");
@ -141,19 +168,43 @@ export function HomeworkScreen({ initialRecordId }: { initialRecordId?: string }
/** `modeOverride` lets the phone flow start straight from a tapped mode card. */
const submit = (modeOverride?: HomeworkMode) => {
const payload = buildPayload(modeOverride ?? mode);
const target = modeOverride ?? mode;
if (target === "check" && !solution.trim()) {
setError({ message: t("homework.solution.required") });
return;
}
const payload = buildPayload(target);
void run(payload);
};
/** Re-run from the stored assignment text (files were never stored). */
const again = (record: HomeworkRecord) => {
setAgainLoading(true);
void run({ mode: record.mode, images: [], text: record.sourceText, recordId: record.id });
void run({
mode: record.mode,
images: [],
text: record.sourceText,
solution: record.mode === "check" ? record.attempt : undefined,
recordId: record.id,
});
};
/** Hand the finished content to the New deck flow with the options pre-filled. */
const turnIntoDeck = (record: HomeworkRecord) => {
const content =
record.mode === "make"
? record.answer
: record.mode === "check"
? record.corrected
: assembleSolution(record);
setDeckSeed({ text: content, difficulty: "medium", count: 10 });
router.push("/create");
};
const newAssignment = () => {
intake.clear();
setText("");
setSolution("");
setError(null);
clearWizardDraft("homework");
setView({ kind: "start" });
@ -164,19 +215,35 @@ export function HomeworkScreen({ initialRecordId }: { initialRecordId?: string }
}
if (view.kind === "record") {
return view.record.mode === "make" ? (
<HomeworkResult
record={view.record}
onAgain={() => again(view.record)}
onNew={newAssignment}
againLoading={againLoading}
/>
) : (
if (view.record.mode === "make") {
return (
<HomeworkResult
record={view.record}
onAgain={() => again(view.record)}
onNew={newAssignment}
onToDeck={() => turnIntoDeck(view.record)}
againLoading={againLoading}
/>
);
}
if (view.record.mode === "check") {
return (
<HomeworkCheckResult
record={view.record}
onAgain={() => again(view.record)}
onNew={newAssignment}
onToDeck={() => turnIntoDeck(view.record)}
againLoading={againLoading}
/>
);
}
return (
<HomeworkGuide
record={view.record}
onClose={() => setView({ kind: "start" })}
onAgain={() => again(view.record)}
onNew={newAssignment}
onToDeck={() => turnIntoDeck(view.record)}
againLoading={againLoading}
/>
);
@ -188,6 +255,8 @@ export function HomeworkScreen({ initialRecordId }: { initialRecordId?: string }
onModeChange={setMode}
text={text}
onTextChange={setText}
solution={solution}
onSolutionChange={setSolution}
intake={intake}
onSubmit={submit}
error={error}