Download src/components/learn-anything/RoadmapPracticeSection.tsx from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 11.7 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/learn-anything/RoadmapPracticeSection.tsx
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/components/learn-anything/RoadmapPracticeSection.tsx
-
curl -L -o RoadmapPracticeSection.tsx https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/learn-anything/RoadmapPracticeSection.tsx
11.7 kB
| "use client"; | |
| /** | |
| * The roadmap's practice loop: a progressive quiz (question first, reveal, | |
| * honest self-mark, score at the end) and spaced-repetition flashcards | |
| * (flip, then "knew it / again" drives a 1d/3d/7d Leitner schedule with a | |
| * due-today queue). Replaces the old static lists that printed answers and | |
| * card backs in plain sight. All state persists through the roadmap the | |
| * same way topics do. Editorial ledger styling, no accent colour. | |
| */ | |
| import Link from "next/link"; | |
| import { useMemo, useState } from "react"; | |
| import { | |
| dueFlashcards, | |
| recordRoadmapQuizResult, | |
| reviewFlashcard, | |
| type LearnAnythingRoadmap, | |
| type RoadmapQuizResult, | |
| } from "@/lib/phase3-learning"; | |
| import { buildStudyIntentUrl } from "@/lib/study-intents"; | |
| export function RoadmapPracticeSection({ | |
| roadmap, | |
| onChange, | |
| onQuizSubmitted, | |
| }: { | |
| roadmap: LearnAnythingRoadmap; | |
| onChange: (next: LearnAnythingRoadmap) => void; | |
| onQuizSubmitted?: (result: RoadmapQuizResult) => void; | |
| }) { | |
| return ( | |
| <section className="mt-12"> | |
| <p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#92949a]">Practice and revision</p> | |
| <p className="mt-4 max-w-[62ch] text-sm font-light leading-relaxed text-[#62635e]">{roadmap.practiceTask}</p> | |
| <FlashcardReview roadmap={roadmap} onChange={onChange} /> | |
| <ProgressiveQuiz roadmap={roadmap} onChange={onChange} onQuizSubmitted={onQuizSubmitted} /> | |
| </section> | |
| ); | |
| } | |
| function FlashcardReview({ | |
| roadmap, | |
| onChange, | |
| }: { | |
| roadmap: LearnAnythingRoadmap; | |
| onChange: (next: LearnAnythingRoadmap) => void; | |
| }) { | |
| const due = useMemo(() => dueFlashcards(roadmap), [roadmap]); | |
| const [reviewing, setReviewing] = useState(false); | |
| const [flipped, setFlipped] = useState(false); | |
| const card = due[0]; | |
| const retired = roadmap.flashcards.filter((c) => roadmap.srState?.[c.front]?.retired).length; | |
| function handleMark(remembered: boolean) { | |
| if (!card) return; | |
| setFlipped(false); | |
| onChange(reviewFlashcard(roadmap, card.front, remembered)); | |
| } | |
| return ( | |
| <div className="mt-8"> | |
| <div className="flex flex-wrap items-baseline justify-between gap-3"> | |
| <p className="text-[11px] font-semibold uppercase tracking-wide text-[#a0a2a8]">Flashcards</p> | |
| <p className="font-mono text-xs text-[#7a7c84]"> | |
| {due.length} due · {retired} retired | |
| </p> | |
| </div> | |
| {!reviewing ? ( | |
| <div className="mt-3 flex flex-wrap items-center justify-between gap-4 rounded-2xl border border-[#e5e5e1] p-5"> | |
| <p className="text-sm font-light leading-relaxed text-[#62635e]"> | |
| {due.length > 0 | |
| ? `${due.length} card${due.length === 1 ? "" : "s"} due for review. Remembered cards come back in 1, 3, then 7 days.` | |
| : "All caught up — nothing due right now. Cards return on their schedule."} | |
| </p> | |
| {due.length > 0 ? ( | |
| <button | |
| type="button" | |
| onClick={() => setReviewing(true)} | |
| className="inline-flex h-10 items-center rounded-lg bg-white px-5 text-sm font-bold text-black transition hover:bg-[#303136]" | |
| > | |
| Review now | |
| </button> | |
| ) : null} | |
| </div> | |
| ) : card ? ( | |
| <div className="mt-3 rounded-2xl border border-[#e5e5e1] bg-white p-6 shadow-[0_1px_2px_rgba(15,17,23,0.03)]"> | |
| <p className="font-mono text-[11px] text-[#a0a2a8]"> | |
| Card · {due.length} left | |
| </p> | |
| <p className="mt-4 text-lg font-semibold leading-snug text-[#17181b]">{card.front}</p> | |
| {flipped ? ( | |
| <> | |
| <p className="mt-4 border-t border-[#e5e5e1] pt-4 text-sm font-light leading-relaxed text-[#45474d]"> | |
| {card.back} | |
| </p> | |
| <div className="mt-6 flex flex-wrap items-center gap-3"> | |
| <button | |
| type="button" | |
| onClick={() => handleMark(true)} | |
| className="inline-flex h-10 items-center rounded-lg bg-white px-5 font-mono text-sm font-bold text-black transition hover:bg-[#303136]" | |
| > | |
| Knew it | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => handleMark(false)} | |
| className="inline-flex h-10 items-center rounded-lg border border-[#deded9] px-5 font-mono text-sm font-semibold text-[#45474d] transition hover:border-white/40 hover:text-[#17181b]" | |
| > | |
| Again | |
| </button> | |
| </div> | |
| </> | |
| ) : ( | |
| <button | |
| type="button" | |
| onClick={() => setFlipped(true)} | |
| className="mt-6 inline-flex h-10 items-center rounded-lg border border-[#deded9] px-5 text-sm font-semibold text-[#45474d] transition hover:border-white/40 hover:text-[#17181b]" | |
| > | |
| Show answer | |
| </button> | |
| )} | |
| </div> | |
| ) : ( | |
| <div className="mt-3 rounded-2xl border border-[#e5e5e1] p-5"> | |
| <p className="text-sm font-light text-[#62635e]"> | |
| Session done — everything due is reviewed. Come back tomorrow. | |
| </p> | |
| <button | |
| type="button" | |
| onClick={() => setReviewing(false)} | |
| className="mt-3 text-sm font-semibold text-[#7a7c84] transition hover:text-[#17181b]" | |
| > | |
| Close | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| function ProgressiveQuiz({ | |
| roadmap, | |
| onChange, | |
| onQuizSubmitted, | |
| }: { | |
| roadmap: LearnAnythingRoadmap; | |
| onChange: (next: LearnAnythingRoadmap) => void; | |
| onQuizSubmitted?: (result: RoadmapQuizResult) => void; | |
| }) { | |
| const questions = roadmap.quiz; | |
| const [state, setState] = useState<"idle" | "running" | "done">("idle"); | |
| const [index, setIndex] = useState(0); | |
| const [revealed, setRevealed] = useState(false); | |
| const [gotIt, setGotIt] = useState<boolean[]>([]); | |
| const last = roadmap.quizResult; | |
| function start() { | |
| setState("running"); | |
| setIndex(0); | |
| setRevealed(false); | |
| setGotIt([]); | |
| } | |
| function mark(correct: boolean) { | |
| const marks = [...gotIt, correct]; | |
| if (index + 1 < questions.length) { | |
| setGotIt(marks); | |
| setIndex(index + 1); | |
| setRevealed(false); | |
| return; | |
| } | |
| const missed: string[] = []; | |
| questions.forEach((q, i) => { | |
| if (!marks[i]) missed.push(q.question); | |
| }); | |
| const next = recordRoadmapQuizResult(roadmap, { | |
| correct: marks.filter(Boolean).length, | |
| total: questions.length, | |
| missedQuestions: missed, | |
| }); | |
| onChange(next); | |
| if (next.quizResult) onQuizSubmitted?.(next.quizResult); | |
| setGotIt(marks); | |
| setState("done"); | |
| } | |
| const question = questions[index]; | |
| const correctCount = gotIt.filter(Boolean).length; | |
| const missed: string[] = []; | |
| questions.forEach((q, i) => { | |
| if (!gotIt[i]) missed.push(q.question); | |
| }); | |
| return ( | |
| <div className="mt-8"> | |
| <div className="flex flex-wrap items-baseline justify-between gap-3"> | |
| <p className="text-[11px] font-semibold uppercase tracking-wide text-[#a0a2a8]">Quiz</p> | |
| {last ? ( | |
| <p className="font-mono text-xs text-[#7a7c84]"> | |
| last run {last.correct}/{last.total} | |
| </p> | |
| ) : null} | |
| </div> | |
| {state === "idle" ? ( | |
| <div className="mt-3 flex flex-wrap items-center justify-between gap-4 rounded-2xl border border-[#e5e5e1] p-5"> | |
| <p className="text-sm font-light leading-relaxed text-[#62635e]"> | |
| {questions.length} questions. See each answer only after committing, then mark yourself honestly. | |
| </p> | |
| <button | |
| type="button" | |
| onClick={start} | |
| className="inline-flex h-10 items-center rounded-lg bg-white px-5 text-sm font-bold text-black transition hover:bg-[#303136]" | |
| > | |
| {last ? "Retake quiz" : "Start quiz"} | |
| </button> | |
| </div> | |
| ) : null} | |
| {state === "running" && question ? ( | |
| <div className="mt-3 rounded-2xl border border-[#e5e5e1] bg-white p-6 shadow-[0_1px_2px_rgba(15,17,23,0.03)]"> | |
| <p className="font-mono text-[11px] text-[#a0a2a8]"> | |
| Q {String(index + 1).padStart(2, "0")} / {String(questions.length).padStart(2, "0")} | |
| </p> | |
| <p className="mt-4 text-lg font-semibold leading-snug text-[#17181b]">{question.question}</p> | |
| {revealed ? ( | |
| <> | |
| <p className="mt-4 border-t border-[#e5e5e1] pt-4 text-sm font-light leading-relaxed text-[#45474d]"> | |
| {question.answer} | |
| </p> | |
| <p className="mt-5 text-[11px] font-semibold uppercase tracking-[0.18em] text-[#92949a]">Did you have it?</p> | |
| <div className="mt-2 flex flex-wrap items-center gap-3"> | |
| <button | |
| type="button" | |
| onClick={() => mark(true)} | |
| className="inline-flex h-10 items-center rounded-lg bg-white px-5 font-mono text-sm font-bold text-black transition hover:bg-[#303136]" | |
| > | |
| Got it | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => mark(false)} | |
| className="inline-flex h-10 items-center rounded-lg border border-[#deded9] px-5 font-mono text-sm font-semibold text-[#45474d] transition hover:border-white/40 hover:text-[#17181b]" | |
| > | |
| Missed it | |
| </button> | |
| </div> | |
| </> | |
| ) : ( | |
| <button | |
| type="button" | |
| onClick={() => setRevealed(true)} | |
| className="mt-6 inline-flex h-10 items-center rounded-lg border border-[#deded9] px-5 text-sm font-semibold text-[#45474d] transition hover:border-white/40 hover:text-[#17181b]" | |
| > | |
| Show answer | |
| </button> | |
| )} | |
| </div> | |
| ) : null} | |
| {state === "done" ? ( | |
| <div className="mt-3 rounded-2xl border border-[#e5e5e1] p-6"> | |
| <p className="font-mono text-2xl font-semibold text-[#17181b]"> | |
| {correctCount}/{questions.length} | |
| </p> | |
| <p className="mt-1 text-sm font-light text-[#6a6b72]"> | |
| {missed.length === 0 ? "Clean run — everything held." : "Missed questions to close out:"} | |
| </p> | |
| {missed.length > 0 ? ( | |
| <> | |
| <ul className="mt-3"> | |
| {missed.map((q) => ( | |
| <li key={q} className="border-b border-[#efefec] py-2 text-sm text-[#45474d]"> | |
| {q} | |
| </li> | |
| ))} | |
| </ul> | |
| <Link | |
| href={buildStudyIntentUrl({ | |
| intent: "generate_quiz", | |
| prompt: `I missed these questions on ${roadmap.topic}: ${missed.join(" | ")}. Reteach the ideas briefly, then quiz me again on them.`, | |
| run: true, | |
| })} | |
| className="mt-4 inline-block text-xs font-semibold text-[#17181b]/65 underline decoration-white/25 underline-offset-4 transition hover:text-[#17181b]" | |
| > | |
| Practice the missed ones in Study Chat | |
| </Link> | |
| </> | |
| ) : null} | |
| <div className="mt-4"> | |
| <button | |
| type="button" | |
| onClick={start} | |
| className="text-sm font-semibold text-[#7a7c84] transition hover:text-[#17181b]" | |
| > | |
| Retake | |
| </button> | |
| </div> | |
| </div> | |
| ) : null} | |
| </div> | |
| ); | |
| } | |