import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { BookOpen, Calendar, ClipboardList, Headphones, Image as ImageIcon, Library, Link2, MessageSquare, Mic, Music, PenSquare, Sparkles, Type, Video, type LucideIcon, } from "lucide-react"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { HoverCard, HoverCardContent, HoverCardTrigger, } from "@/components/ui/hover-card"; import { withAuthQuery } from "@/lib/api-client"; import MaterialBookView, { SkillBadge, } from "@/components/coursePlan/MaterialBookView"; import type { WorkbookMode } from "@/components/coursePlan/InteractiveWorkbook"; import type { CoursePlan, CoursePlanMaterial, CoursePlanMedia, CoursePlanWeek, } from "@/types"; const SKILL_ICONS: Record = { reading: BookOpen, writing: PenSquare, listening: Headphones, speaking: Mic, grammar: Type, vocabulary: Library, integrated: MessageSquare, }; /** * Read-only renderer for a course plan that BOTH the student page and * the admin "View as Student" preview share. The only difference is * the workbook `mode`: * * - `mode="student"` → answers persist server-side (real attempts). * - `mode="preview"` → InteractiveWorkbook runs in preview mode (no * persistence) and surfaces an "Show answer key" toggle so admins * can verify the v2 generator's output. * * Materials are rendered through `MaterialBookView`, which dispatches * `interactive_workbook` materials into the live workbook component. */ export interface PlanReaderProps { plan: CoursePlan; mode?: WorkbookMode; /** Heading level for accessibility — admins use this inside their own * Card; students use it as the page's main grid block. Defaults to 'h3'. */ headingClassName?: string; } export default function PlanReader({ plan, mode = "student" }: PlanReaderProps) { const { t } = useTranslation(); const materialsByWeek = useMemo(() => { const map = new Map(); if (!plan.materials) return map; for (const m of plan.materials) { const list = map.get(m.week_number) ?? []; list.push(m); map.set(m.week_number, list); } return map; }, [plan.materials]); return (
{plan.name} {plan.description && ( {plan.description} )}
{plan.cefr_level} {t("coursePlan.weeksCount", { count: plan.total_weeks })} {t("coursePlan.hoursPerWeek", { count: plan.contact_hours_per_week, })}
{plan.assignment && (

{plan.assignment.due_date ? t("coursePlan.student.due", { date: plan.assignment.due_date }) : t("coursePlan.student.noDue")} {plan.assignment.assigned_by_name && ( ·{" "} {t("coursePlan.student.assignedBy", { name: plan.assignment.assigned_by_name, })} )}

)} {plan.assignment?.message && (

{plan.assignment.message}

)}
{plan.objectives && plan.objectives.length > 0 && ( {t("coursePlan.sections.objectives")}
    {plan.objectives.map((o, i) => (
  1. {o}
  2. ))}
)} {plan.weeks && plan.weeks.length > 0 && ( {t("coursePlan.sections.delivery")} {t("coursePlan.deliveryHint")} {plan.weeks.map((w) => ( ))} )}
); } function ReaderWeek({ week, materials, mode, }: { week: CoursePlanWeek; materials: CoursePlanMaterial[]; mode: WorkbookMode; }) { const { t } = useTranslation(); const [skillFilter, setSkillFilter] = useState("all"); const skills = useMemo( () => Array.from(new Set(materials.map((m) => m.skill))).sort(), [materials], ); const filteredMaterials = useMemo( () => materials.filter( (m) => skillFilter === "all" || m.skill === skillFilter, ), [materials, skillFilter], ); return (
{t("coursePlan.weekN", { n: week.week_number })}
{week.focus || week.unit || "—"}
{week.date_label && (
{week.date_label}
)}
{skills.length > 0 && (
{skills.map((s) => ( ))}
)} {materials.length === 0 && (

{t("coursePlan.media.noMedia")}

)} {filteredMaterials.map((m) => ( ))}
); } function ReaderMaterial({ material, mode, }: { material: CoursePlanMaterial; mode: WorkbookMode; }) { const { t } = useTranslation(); const Icon = SKILL_ICONS[material.skill] ?? ClipboardList; return (
{material.title} {t( `coursePlan.materialType.${material.material_type}`, material.material_type, )}
{material.summary && ( {material.summary} )} {material.share_date && ( {t("coursePlan.shareDate", "Share date")}: {material.share_date} )}
{(material.media ?? []).map((m) => ( ))}
); } function ReaderMediaTile({ media }: { media: CoursePlanMedia }) { const url = withAuthQuery(media.preview_url || media.download_url || ""); if (!url) return null; return (
{media.kind === "audio" && } {media.kind === "image" && } {media.kind === "video" &&
{media.kind === "audio" &&
); } /** Pill that surfaces RAG / extraction provenance for one material. */ export function GroundingBadge({ material }: { material: CoursePlanMaterial }) { const grounded = material.grounded_on ?? []; const extracted = material.extracted_from ?? null; if (!grounded.length && !extracted) return null; const total = grounded.reduce((acc, g) => acc + (g.chunks_used || 0), 0) + (extracted ? 1 : 0); return ( Grounded on {total} reference{total === 1 ? "" : "s"} {grounded.length > 0 && (
RAG sources
    {grounded.map((g) => (
  • {g.title || `Source #${g.source_id}`} {g.chunks_used} chunk(s)
  • ))}
)} {extracted && (
Extracted from
{extracted.source_title || `Source #${extracted.source_id}`} {extracted.page_hint ? ` · ${extracted.page_hint}` : ""}
{extracted.topic_keywords && extracted.topic_keywords.length > 0 && (
Topics: {extracted.topic_keywords.slice(0, 5).join(", ")}
)}
)}
); }