DocDoeAI / src /components /student-os /ChapterPractice.tsx
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
5.6 kB
"use client";
import { useState } from "react";
import Link from "next/link";
import { useStudentWorkspace } from "./StudentWorkspaceProvider";
type Question = { id: string; question: string; answer: string };
export function ChapterPractice({ chapterId, title, subject, questions }: {
chapterId: string; title: string; subject: string; questions: Question[];
}) {
const { updateWorkspace, status } = useStudentWorkspace();
const [index, setIndex] = useState(0);
const [drafts, setDrafts] = useState<Record<string, string>>({});
const [revealed, setRevealed] = useState<Record<string, boolean>>({});
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const question = questions[index];
if (!question) return <p>Practice questions for this chapter are being prepared. You can study the notes and formulas above.</p>;
const draft = drafts[question.id] ?? "";
const shown = revealed[question.id] ?? false;
async function saveAttempt() {
if (saving || !draft.trim()) return;
setSaving(true);
setError(null);
setMessage(null);
const now = new Date().toISOString();
const id = `chapter-practice:${chapterId}:${question.id}`;
try {
await updateWorkspace((current) => {
const existing = current.notes.find((note) => note.id === id);
const note = {
id, title: `${title} · Practice ${index + 1}`,
body: `Question\n${question.question}\n\nMy answer\n${draft.trim()}\n\nReference answer\n${question.answer}\n\nSelf-check practice. Not automatically graded.`,
subject, topic: title, source_id: null, source_label: "DocDoe chapter sample practice",
favorite: existing?.favorite ?? false, created_at: existing?.created_at ?? now, updated_at: now,
};
return { ...current, notes: [note, ...current.notes.filter((item) => item.id !== id)] };
});
setMessage("Attempt saved to Notes. Saving again updates this question's note.");
} catch {
setError("Could not sync this attempt. Your answer is still here; retry when connected.");
} finally {
setSaving(false);
}
}
function move(next: number) {
setIndex(next);
setMessage(null);
setError(null);
}
return <div className="border-y border-[#e3e1da] py-5">
<div className="mb-4 flex flex-wrap items-center justify-between gap-3 text-xs text-[#6f6e68]">
<p>Question {index + 1} of {questions.length}</p>
<p>Self-check · not graded</p>
</div>
<h3 id={`practice-question-${chapterId}`} className="text-base font-semibold leading-7 text-[#171716]">{question.question}</h3>
<label className="mt-5 block text-sm font-medium" htmlFor={`practice-answer-${chapterId}`}>Your answer</label>
<textarea id={`practice-answer-${chapterId}`} aria-describedby={`practice-question-${chapterId}`} value={draft}
onChange={(event) => { setDrafts((current) => ({ ...current, [question.id]: event.target.value })); setMessage(null); }}
placeholder="Try from memory first. Use the notes above if you get stuck."
className="mt-2 min-h-32 w-full resize-none rounded-lg border border-[#d6d4cc] bg-white p-3 text-sm leading-6 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#171716]" />
<p className="text-xs text-[#6f6e68]">Drafts stay on this page only. Save to Notes before leaving.</p>
<button type="button" onClick={() => setRevealed((current) => ({ ...current, [question.id]: !shown }))}
aria-expanded={shown} aria-controls={`practice-reference-${chapterId}`}
className="mt-4 min-h-11 rounded-lg bg-[#171716] px-4 text-sm font-semibold text-white hover:bg-[#343432] focus-visible:outline-2 focus-visible:outline-offset-2">
{shown ? "Hide reference answer" : draft.trim() ? "Compare my answer" : "I need help · show answer"}
</button>
{shown && <div id={`practice-reference-${chapterId}`} className="mt-5 border-l-2 border-[#171716] pl-4">
<h4 className="font-semibold">Reference answer</h4>
<p className="mt-2 whitespace-pre-line">{question.answer}</p>
<p className="mt-3 text-sm text-[#6f6e68]">Check your reasoning and keywords. Rewrite anything you missed before saving.</p>
</div>}
<div className="mt-5 flex flex-wrap items-center gap-3">
<button type="button" onClick={() => void saveAttempt()} disabled={saving || !draft.trim() || status === "loading"} aria-busy={saving}
className="min-h-11 rounded-lg border border-[#d6d4cc] px-4 text-sm font-semibold disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-2 focus-visible:outline-offset-2">
{saving ? "Saving…" : "Save attempt to Notes"}
</button>
<Link href="/notes" className="inline-flex min-h-11 items-center text-sm underline">Open Notes</Link>
</div>
{message && <p role="status" className="mt-3 text-sm">{message}</p>}
{error && <p role="alert" className="mt-3 text-sm text-[#b33a3a]">{error}</p>}
<nav aria-label="Practice questions" className="mt-6 flex justify-between gap-4 border-t border-[#e3e1da] pt-4">
<button type="button" disabled={index === 0 || saving} onClick={() => move(index - 1)} className="min-h-11 px-2 text-sm underline disabled:opacity-40">Previous question</button>
<button type="button" disabled={index === questions.length - 1 || saving} onClick={() => move(index + 1)} className="min-h-11 px-2 text-sm underline disabled:opacity-40">Next question</button>
</nav>
</div>;
}