"use client"; import { useMemo, useState } from "react"; import Link from "next/link"; import { AiDisclaimer } from "@/components/AiDisclaimer"; import { Button, Spinner } from "@/components/ui"; import { RefreshIcon, SparklesIcon } from "@/components/icons"; import { ApiError, generateDeck } from "@/lib/client-api"; import { deckHasNotes, generatedDeckText } from "@/lib/chat-context"; import { materialiseActivities, activityTypes } from "@/lib/deck-ops"; import { scheduleForReview } from "@/lib/history"; import { useI18n } from "@/lib/i18n"; import type { Deck, TeachCardRef, TeachDifficulty, TeachSummary } from "@/lib/schemas"; import { upsertDeck } from "@/lib/storage"; import { gapCardFrom, gapFocus } from "@/lib/teach"; import { showToast } from "@/lib/toast"; import { cn } from "@/lib/utils"; type ProposedCard = { id: string; front: string; back: string; include: boolean }; /** * The recap of a Teach it back session: what was explained well, what was * missed or unclear (linked to the user's own cards when one covers it), and * the next steps — plus the two follow-ups the recap feeds: putting the missed * cards on the review schedule, and turning the gaps into new flashcards * (generated from the deck's own notes, editable before saving). */ export function TeachRecap({ topic, difficulty, cards, decks, summary, busy, error, onRetry, onBackToSession, }: { topic: string; difficulty: TeachDifficulty; cards: TeachCardRef[]; decks: Deck[]; summary: TeachSummary | null; busy: boolean; error: string | null; onRetry: () => void; onBackToSession: () => void; }) { const { t, plural } = useI18n(); /* Cards the recap linked a gap to — the ones "add to review" schedules. */ const linked = useMemo(() => { const refs = new Set((summary?.gaps ?? []).map((gap) => gap.ref).filter((ref): ref is number => ref != null)); return cards.filter((card) => refs.has(card.ref)); }, [summary, cards]); const addToReview = () => { const byDeck = new Map(); for (const card of linked) { const list = byDeck.get(card.deckId) ?? []; list.push(card.activityId); byDeck.set(card.deckId, list); } let count = 0; for (const [deckId, activityIds] of byDeck) count += scheduleForReview(deckId, activityIds); showToast({ message: count > 0 ? plural(count, "teach.reviewAdded") : t("teach.gapsEmpty") }); }; const defaultDeckId = linked[0]?.deckId ?? cards[0]?.deckId ?? decks[0]?.id ?? ""; return (

{topic} · {difficulty === "friendly" ? t("teach.friendly") : t("teach.skeptical")}

{busy ? (
{t("teach.summaryBusy")}
) : null} {!busy && error ? (

{error}

) : null} {!busy && summary ? ( <>

{t("teach.gaps")}

{summary.gaps.length === 0 ? (

{t("teach.gapsEmpty")}

) : (
    {summary.gaps.map((gap, index) => { const card = gap.ref != null ? cards.find((entry) => entry.ref === gap.ref) : undefined; return (
  • {gap.what} {card ? ( {card.deckTitle} {card.label} ) : null}
  • ); })}
)}
{linked.length > 0 ? ( ) : null} {summary.gaps.length > 0 ? ( ) : null}
) : null}
); } function RecapList({ title, tone, items, empty }: { title: string; tone: "good" | "next"; items: string[]; empty: string }) { return (

{title}

{items.length === 0 ? (

{empty}

) : ( )}
); } /** * "Turn gaps into cards": one flashcard per gap, generated from the chosen * deck's own notes, reviewed and edited here before anything is saved. */ function GapCards({ topic, gaps, decks, defaultDeckId, }: { topic: string; gaps: { what: string }[]; decks: Deck[]; defaultDeckId: string; }) { const { t, plural } = useI18n(); const [open, setOpen] = useState(false); const [deckId, setDeckId] = useState(defaultDeckId); const [generating, setGenerating] = useState(false); const [generateError, setGenerateError] = useState(null); const [proposed, setProposed] = useState(null); const [savedCount, setSavedCount] = useState(null); const deck = decks.find((entry) => entry.id === deckId); const chosen = (proposed ?? []).filter((card) => card.include && card.front.trim() && card.back.trim()); const patch = (id: string, change: Partial) => setProposed((current) => current?.map((card) => (card.id === id ? { ...card, ...change } : card)) ?? null); const generate = async () => { if (!deck || generating) return; setGenerating(true); setGenerateError(null); try { const { deck: generated } = await generateDeck({ text: (deckHasNotes(deck) ? deck.sourceText : generatedDeckText(deck)).slice(0, 48_000), difficulty: deck.difficulty, count: Math.max(4, Math.min(8, gaps.length * 2)), focus: gapFocus(gaps), language: deck.language, mode: "more", existing: { title: deck.title, types: activityTypes(deck.activities), count: deck.activities.length }, }); const cards = generated.activities .map((activity, index) => { const pair = gapCardFrom(activity); return { id: `gap-${index}`, ...pair, include: true }; }) .filter((card) => card.front.length > 0 && card.back.length > 0); if (cards.length === 0) { setGenerateError(t("teach.generateFailed")); return; } setProposed(cards); } catch (caught) { setGenerateError(caught instanceof ApiError ? caught.message : t("teach.generateFailed")); } finally { setGenerating(false); } }; const save = () => { if (!deck || chosen.length === 0) return; const activities = materialiseActivities( chosen.map((card) => ({ type: "flashcard" as const, topic: topic.slice(0, 120) || undefined, front: card.front.trim().slice(0, 400), back: card.back.trim().slice(0, 1200), explanation: gaps[0]?.what.slice(0, 600) ?? t("teach.gapsToCards"), })), deck.difficulty, ); upsertDeck({ ...deck, activities: [...deck.activities, ...activities], updatedAt: Date.now() }); setProposed(null); setSavedCount(activities.length); showToast({ message: plural(activities.length, "teach.cardsSaved", { deck: deck.title }) }); }; if (!open) { return ( ); } return (

{t("teach.gapsToCards")}

{savedCount !== null ? (

{plural(savedCount, "teach.cardsSaved", { deck: deck?.title ?? "" })}

) : null} {!proposed ? ( <> {generateError ? (

{generateError}

) : null}
) : ( <>

{t("teach.reviewTitle")}

{t("teach.reviewHint")}

    {proposed.map((card) => (
  • patch(card.id, { include: event.target.checked })} aria-label={t("teach.reviewTitle")} className="size-4 accent-sky-500" /> {t("teach.cardFront")}
    patch(card.id, { front: event.target.value })} aria-label={t("teach.cardFront")} maxLength={400} className="w-full rounded-[var(--radius-chip)] border border-[var(--glass-border)] bg-[var(--surface)] px-2.5 py-2 text-[16px] text-[var(--text)] outline-none focus:border-accent-400/60 sm:text-[13px]" /> {t("teach.cardBack")}