"use client"; import { motion, useReducedMotion } from "framer-motion"; import { createContext, useContext } from "react"; import { useI18n } from "@/lib/i18n"; import type { Activity } from "@/lib/schemas"; import { useIsPhone } from "@/lib/use-phone"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui"; export type ActivityComponentProps = { activity: A; /** True when this slide is the one currently filling the screen. */ active: boolean; /** Language of the deck, so AI grading answers in the same language. */ language?: string; /** Report the result to the feed (once per attempt). */ onAnswered: (correct: boolean, meta?: { userAnswer?: string }) => void; /** Scroll the feed to the next slide. */ onNext: () => void; isLast: boolean; /** Overrides the feedback button label (the homework guide uses this). */ nextLabel?: string; /** Exam mode: hide hint rows and answer-reveal affordances. */ noHints?: boolean; }; export const TYPE_META: Record = { flashcard: { labelKey: "type.flashcard", icon: "🃏", tint: "tint-sky" }, multiple_choice: { labelKey: "type.multipleChoice", icon: "🔘", tint: "tint-lilac" }, true_false: { labelKey: "type.trueFalse", icon: "⚖️", tint: "tint-mint" }, fill_blank: { labelKey: "type.fillBlank", icon: "✏️", tint: "tint-peach" }, matching: { labelKey: "type.matching", icon: "🧲", tint: "tint-sky" }, ordering: { labelKey: "type.ordering", icon: "🔢", tint: "tint-lilac" }, short_answer: { labelKey: "type.shortAnswer", icon: "💬", tint: "tint-rose" }, }; export function ActivityHeader({ type, topic, extra, }: { type: Activity["type"]; topic?: string; extra?: React.ReactNode; }) { const { t } = useI18n(); const meta = TYPE_META[type]; return (
{meta.icon} {t(meta.labelKey)} {topic ? ( {topic} ) : null} {extra}
); } export function PromptCard({ children, className, style, }: { children: React.ReactNode; className?: string; style?: React.CSSProperties; }) { return (
{children}
); } export function QuestionText({ children, className }: { children: React.ReactNode; className?: string }) { return (

{children}

); } /** Standard tile used for answer options. */ export function OptionTile({ children, state = "idle", disabled, onClick, className, shortcut, }: { children: React.ReactNode; state?: "idle" | "selected" | "correct" | "wrong" | "dim"; disabled?: boolean; onClick?: () => void; className?: string; shortcut?: string; }) { const states: Record = { idle: "border-white/60 bg-white/55 hover:bg-white/80 dark:border-white/12 dark:bg-white/[0.07] dark:hover:bg-white/[0.12]", selected: "border-accent-400 bg-accent-50/80 dark:bg-accent-500/20", correct: "border-emerald-300 bg-emerald-50/90 text-emerald-950 dark:border-emerald-400/50 dark:bg-emerald-400/15 dark:text-emerald-50", wrong: "border-rose-300 bg-rose-50/90 text-rose-950 dark:border-rose-400/50 dark:bg-rose-400/15 dark:text-rose-50", dim: "border-white/40 bg-white/35 text-frost-400 dark:border-white/10 dark:bg-white/[0.04] dark:text-frost-500", }; return ( ); } export function FeedbackPanel({ correct, explanation, xp, onNext, isLast, nextLabel, headline, detail, children, }: { correct: boolean; explanation: string; xp?: number; onNext: () => void; isLast?: boolean; /** Overrides the default “Next” / “See results” label. */ nextLabel?: string; headline?: string; detail?: React.ReactNode; children?: React.ReactNode; }) { const { t } = useI18n(); const isPhone = useIsPhone(); const reduceMotion = useReducedMotion(); return (

{correct ? "✅" : "💧"} {headline ?? (correct ? t("activity.niceOne") : t("activity.notQuite"))}

{correct && xp ? ( +{xp} XP ) : null}
{detail ?
{detail}
: null}

{t("activity.why")} {explanation}

{children}
); } /** Small hint row under a question, e.g. "Tap a term, then its definition". */ export function Hint({ children }: { children: React.ReactNode }) { const noHints = useNoHints(); if (noHints) return null; // hint-line: Aero gives this its own glass chip (it sits on the wallpaper). return

{children}

; } /** * Exam mode sets this through ActivityRenderer's `noHints` prop so the * affordances that give the answer away disappear without touching every * activity component. */ const NoHintsContext = createContext(false); export function NoHintsProvider({ value, children }: { value: boolean; children: React.ReactNode }) { return {children}; } export function useNoHints(): boolean { return useContext(NoHintsContext); }