"use client"; import { motion } from "framer-motion"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ActivityRenderer } from "@/components/activities"; import { PromptCard, QuestionText } from "@/components/activities/shared"; import { AiDisclaimer } from "@/components/AiDisclaimer"; import { XIcon } from "@/components/icons"; import { Button, ProgressBar } from "@/components/ui"; import { AskButton } from "@/components/chat/AskButton"; import { assembleSolution } from "@/lib/homework"; import { useI18n } from "@/lib/i18n"; import type { HomeworkGuideRecord, HomeworkStep } from "@/lib/schemas"; import { useWakeLock } from "@/lib/use-wake-lock"; import { refineHomework } from "@/lib/client-api"; import { CopyButton } from "./CopyButton"; /** * Mode 2: one step per screen in the same snap-feed as the study feed. * Each step explains what to do, reveals hints one at a time (nudge → hint → * the full answer), and can carry a checkpoint rendered by the shared activity * components. The last screen assembles the solution from the step answers. */ export function HomeworkGuide({ record, onClose, onAgain, onNew, onToDeck, againLoading, }: { record: HomeworkGuideRecord; onClose: () => void; onAgain: () => void; onNew: () => void; onToDeck: () => void; againLoading: boolean; }) { const { t } = useI18n(); /* Keep the screen awake while working through the guide. */ useWakeLock(true); const steps = record.steps; const slideCount = steps.length + 1; // + the solution slide const [index, setIndex] = useState(0); const [revealed, setRevealed] = useState>({}); /** "Explain this step differently" overrides, per step index. */ const [explained, setExplained] = useState>({}); const [explaining, setExplaining] = useState(null); const [explainFailed, setExplainFailed] = useState(null); const scrollRef = useRef(null); const explainStep = async (stepIndex: number) => { if (explaining !== null) return; setExplaining(stepIndex); setExplainFailed(null); try { const next = await refineHomework({ instruction: "explain", text: steps[stepIndex].explanation, context: record.sourceText, }); setExplained((current) => ({ ...current, [stepIndex]: next })); } catch { setExplainFailed(stepIndex); } finally { setExplaining(null); } }; const goTo = useCallback( (target: number) => { const container = scrollRef.current; if (!container) return; const clamped = Math.max(0, Math.min(target, slideCount - 1)); container.scrollTo({ top: clamped * container.clientHeight, behavior: "smooth" }); }, [slideCount], ); // Track the visible slide (wheel, touch and programmatic scroll). useEffect(() => { const container = scrollRef.current; if (!container) return; let frame = 0; const onScroll = () => { cancelAnimationFrame(frame); frame = requestAnimationFrame(() => { const height = container.clientHeight || 1; const current = Math.round(container.scrollTop / height); setIndex((previous) => (previous === current ? previous : current)); }); }; container.addEventListener("scroll", onScroll, { passive: true }); return () => { container.removeEventListener("scroll", onScroll); cancelAnimationFrame(frame); }; }, []); // Keyboard navigation, like the study feed. useEffect(() => { const onKey = (event: KeyboardEvent) => { const target = event.target as HTMLElement | null; if (target && (/^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName) || target.isContentEditable)) return; if (event.metaKey || event.ctrlKey || event.altKey) return; if (event.key === "ArrowDown" || event.key === "PageDown" || event.key === "j") { event.preventDefault(); goTo(index + 1); } else if (event.key === "ArrowUp" || event.key === "PageUp" || event.key === "k") { event.preventDefault(); goTo(index - 1); } else if (event.key === "Escape") { onClose(); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [goTo, index, onClose]); const solution = useMemo(() => assembleSolution(record), [record]); const progressValue = Math.min(index + 1, slideCount); const onSolution = index >= steps.length; const revealedCount = (stepIndex: number) => revealed[stepIndex] ?? 0; const revealNext = (stepIndex: number, hints: string[]) => setRevealed((current) => ({ ...current, [stepIndex]: Math.min((current[stepIndex] ?? 0) + 1, hints.length), })); const hintButtonLabel = (step: HomeworkStep, stepIndex: number) => { const count = revealedCount(stepIndex); if (count === 0) return t("homework.guide.hint"); if (count === step.hints.length - 1) return t("homework.guide.hintAnswer"); return t("homework.guide.hintMore"); }; return (
{/* chrome ------------------------------------------------------- */} {/* Same pill as the study feed: round close button, title + step above the bar (nothing crowds the rounded ends). */}
{record.title} {onSolution ? t("homework.guide.solution") : t("homework.guide.stepOf", { n: index + 1, total: steps.length })}
{/* slides ------------------------------------------------------- */}
{steps.map((step, stepIndex) => (
{t("homework.guide.stepOf", { n: stepIndex + 1, total: steps.length })}
{step.title} {/* The model writes plain text with short lines and numbered lists (see HOMEWORK_BASE_RULES) — keep its line breaks so the step reads as steps, not one run-on paragraph. */}

{explained[stepIndex] ?? step.explanation}

{explainFailed === stepIndex ? ( {t("homework.followup.failed")} ) : null}
{/* progressive hints */}
{step.hints.slice(0, revealedCount(stepIndex)).map((hint, hintIndex) => ( {t("homework.guide.hintN", { n: hintIndex + 1 })}: {hint} ))} {revealedCount(stepIndex) < step.hints.length ? ( ) : null}
{/* checkpoint or a plain next button */} {step.checkpoint ? (

{t("homework.guide.tryIt")}

{ /* checkpoints are for the student, not for scoring */ }} onNext={() => goTo(stepIndex + 1)} />
) : ( )}
))} {/* the assembled solution -------------------------------------- */}
{t("homework.guide.solution")}

{solution}

{t("homework.guide.solutionNote")}

`${index + 1}. ${step.title}${step.explanation ? ` — ${step.explanation}` : ""}`), ].join("\n"), }} />

{t("homework.toDeckNote")}

); }