Download src/components/student-os/ChapterPractice.tsx from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 5.6 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/student-os/ChapterPractice.tsx
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/components/student-os/ChapterPractice.tsx
-
curl -L -o ChapterPractice.tsx https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/student-os/ChapterPractice.tsx
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>; | |
| } | |