"use client"; import { useCallback, useEffect, useRef, useState, type RefObject } from "react"; import { CHAPTER_META, FLASHCARDS, FORMULA_LOCK, NOTE_SECTIONS, QUIZ, matchChatReply, type Flashcard, type QuizQuestion, } from "@/content/sslc/physics/sound-waves/oneshot"; const display = "var(--font-docdoe-display), Archivo, ui-sans-serif, system-ui, sans-serif"; const body = "var(--font-docdoe-sans), Onest, ui-sans-serif, system-ui, sans-serif"; type StepId = "watch" | "notes" | "chat" | "test"; const STEPS: { id: StepId; label: string }[] = [ { id: "watch", label: "Watch" }, { id: "notes", label: "Notes" }, { id: "chat", label: "Chat" }, { id: "test", label: "Test" }, ]; const WATCH_UNLOCK_SEC = 20; const LS_DONE = "docdoe.preview.sound-waves.chapterDone"; type ChatMsg = { role: "tutor" | "student"; text: string }; const CHIPS = ["Explain again", "Give example", "Exam trap", "Quiz me"] as const; export function SoundWavesChapterLoop() { const [step, setStep] = useState("watch"); const [watchedEnough, setWatchedEnough] = useState(false); const [founderSkip, setFounderSkip] = useState(false); const notesUnlocked = watchedEnough || founderSkip; const videoRef = useRef(null); const onTimeUpdate = useCallback(() => { const v = videoRef.current; if (!v) return; if (v.currentTime >= WATCH_UNLOCK_SEC) setWatchedEnough(true); }, []); const go = (id: StepId) => { if (id !== "watch" && !notesUnlocked && id === "notes") return; if ((id === "notes" || id === "chat" || id === "test") && !notesUnlocked) return; // Allow chat/test only after notes unlocked (same gate as notes for prototype simplicity) setStep(id); }; const unlockAndNotes = () => { setFounderSkip(true); setStep("notes"); }; return (

Sound Waves chapter loop preview

Sound Waves chapter loop preview · /home untouched

{CHAPTER_META.subject} · {CHAPTER_META.board}

{CHAPTER_META.title}

{step === "watch" ? ( setStep("notes")} onFounderSkip={unlockAndNotes} /> ) : null} {step === "notes" ? setStep("chat")} /> : null} {step === "chat" ? setStep("test")} /> : null} {step === "test" ? : null}
); } function WatchStep({ videoRef, onTimeUpdate, notesUnlocked, onOpenNotes, onFounderSkip, }: { videoRef: RefObject; onTimeUpdate: () => void; notesUnlocked: boolean; onOpenNotes: () => void; onFounderSkip: () => void; }) { return (

Watch the class first. Notes open after.

); } function NotesStep({ onContinue }: { onContinue: () => void }) { return (

One-shot notes

{NOTE_SECTIONS.map((s) => (

{s.n}. {s.title}

{s.concept}

{s.formula ? (
                {s.formula}
              
) : null} {s.trap ? (

Trap · {s.trap}

) : null} {s.example ? (

Example · {s.example}

) : null}
))}

Chapter formula lock

Write these without looking.

    {FORMULA_LOCK.map((line) => (
  • {line}
  • ))}
); } function ChatStep({ onContinue }: { onContinue: () => void }) { const [messages, setMessages] = useState([ { role: "tutor", text: `Locked to ${CHAPTER_META.contextLock}. Ask a trap, a formula, or tap a chip.`, }, ]); const [draft, setDraft] = useState(""); const bottomRef = useRef(null); useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); const send = (text: string) => { const cleaned = text.trim(); if (!cleaned) return; const reply = matchChatReply(cleaned); setMessages((m) => [...m, { role: "student", text: cleaned }, { role: "tutor", text: reply }]); setDraft(""); }; return (

Study chat

{CHAPTER_META.contextLock}
{messages.map((m, i) => (
{m.role === "tutor" ? (

Tutor

) : null} {m.text}
))}
{CHIPS.map((c) => ( ))}
{ e.preventDefault(); send(draft); }} > setDraft(e.target.value)} placeholder="Ask about Sound Waves…" className="min-h-11 flex-1 rounded-full border border-white/12 bg-white/[0.06] px-4 text-[14px] text-[#f2f4f1] outline-none placeholder:text-[#f2f4f1]/35 focus:border-[#c6e04a]/50" />
); } function TestStep() { const [qi, setQi] = useState(0); const [selected, setSelected] = useState(null); const [revealed, setRevealed] = useState(false); const [score, setScore] = useState(0); const [phase, setPhase] = useState<"quiz" | "score" | "cards">("quiz"); const [cardIndex, setCardIndex] = useState(0); const [flipped, setFlipped] = useState(false); const [knowCount, setKnowCount] = useState(0); const [done, setDone] = useState(() => { if (typeof window === "undefined") return false; try { return window.localStorage.getItem(LS_DONE) === "1"; } catch { return false; } }); const q: QuizQuestion = QUIZ[qi]; const card: Flashcard = FLASHCARDS[cardIndex]; const total = QUIZ.length; const submitChoice = (idx: number) => { if (revealed) return; setSelected(idx); setRevealed(true); if (idx === q.answerIndex) setScore((s) => s + 1); }; const nextQ = () => { if (qi + 1 >= total) { setPhase("score"); return; } setQi((i) => i + 1); setSelected(null); setRevealed(false); }; const retryType = () => { setSelected(null); setRevealed(false); if (selected === q.answerIndex) setScore((s) => Math.max(0, s - 1)); }; const markDone = () => { try { localStorage.setItem(LS_DONE, "1"); setDone(true); } catch { setDone(true); } }; const advanceCard = (know: boolean) => { if (know) setKnowCount((c) => c + 1); setFlipped(false); if (cardIndex + 1 >= FLASHCARDS.length) { setCardIndex(0); return; } setCardIndex((i) => i + 1); }; const scoreLine = `${score} / ${total}`; if (phase === "quiz") { return (

Exam test

{qi + 1} / {total}

{q.prompt}

{q.choices.map((c, idx) => { const isSel = selected === idx; const isAns = idx === q.answerIndex; let cls = "w-full rounded-[12px] border border-[#10211c]/12 bg-[#f2f4f1] px-3 py-3 text-left text-[14px] leading-5"; if (revealed && isAns) cls += " border-[#10211c] bg-[#c6e04a]/35"; if (revealed && isSel && !isAns) cls += " border-red-700/40 bg-red-50"; if (!revealed) cls += " hover:border-[#10211c]/35"; return ( ); })}
{revealed ? (

Fix · {q.fix}

) : null}
); } if (phase === "score") { return (

Score

{scoreLine}

Next: flashcards from the chapter formula lock.

); } return (

Formula flashcards

{cardIndex + 1} / {FLASHCARDS.length} · know {knowCount}

localStorage only · account progress untouched

); }