"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 (
Practice and revision
{roadmap.practiceTask}
);
}
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 (
Flashcards
{due.length} due · {retired} retired
{!reviewing ? (
{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."}
{due.length > 0 ? (
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
) : null}
) : card ? (
Card · {due.length} left
{card.front}
{flipped ? (
<>
{card.back}
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
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
>
) : (
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
)}
) : (
Session done — everything due is reviewed. Come back tomorrow.
setReviewing(false)}
className="mt-3 text-sm font-semibold text-[#7a7c84] transition hover:text-[#17181b]"
>
Close
)}
);
}
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([]);
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 (
Quiz
{last ? (
last run {last.correct}/{last.total}
) : null}
{state === "idle" ? (
{questions.length} questions. See each answer only after committing, then mark yourself honestly.
{last ? "Retake quiz" : "Start quiz"}
) : null}
{state === "running" && question ? (
Q {String(index + 1).padStart(2, "0")} / {String(questions.length).padStart(2, "0")}
{question.question}
{revealed ? (
<>
{question.answer}
Did you have it?
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
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
>
) : (
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
)}
) : null}
{state === "done" ? (
{correctCount}/{questions.length}
{missed.length === 0 ? "Clean run — everything held." : "Missed questions to close out:"}
{missed.length > 0 ? (
<>
{missed.map((q) => (
{q}
))}
Practice the missed ones in Study Chat
>
) : null}
Retake
) : null}
);
}