import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import { Check, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Slider } from "@/components/ui/slider"; import { Separator } from "@/components/ui/separator"; import { Skeleton } from "@/components/ui/skeleton"; import { useIeltsContentPool, useIeltsAutoAssemble, useIeltsSkills } from "@/hooks/queries/useExamTemplates"; import { ieltsExamService } from "@/services/ielts-exam.service"; import type { ContentPoolFilters, ContentPoolItem, ExamDifficulty, IELTSSkill, IELTSSkillConfig } from "@/types"; import { toast } from "sonner"; const SKILLS: { value: IELTSSkill; label: string }[] = [ { value: "listening", label: "Listening" }, { value: "reading", label: "Reading" }, { value: "writing", label: "Writing" }, { value: "speaking", label: "Speaking" }, ]; function difficultyLabel(d: ExamDifficulty): string { return d.charAt(0).toUpperCase() + d.slice(1); } export default function IeltsContentPool() { const { examId: examIdParam } = useParams<{ examId: string }>(); const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const examId = Number(examIdParam); const [skill, setSkill] = useState(() => (searchParams.get("skill") as IELTSSkill) || "listening"); const [part, setPart] = useState(() => Number(searchParams.get("part")) || 1); const [difficultyIdx, setDifficultyIdx] = useState(1); const difficulties: ExamDifficulty[] = ["easy", "medium", "hard"]; const difficulty = difficulties[difficultyIdx] ?? "medium"; const [topicInput, setTopicInput] = useState(""); const [search, setSearch] = useState(""); const [selected, setSelected] = useState([]); const [pendingHybrid, setPendingHybrid] = useState([]); const assemblyMode = useMemo(() => { if (!Number.isFinite(examId)) return "manual"; return sessionStorage.getItem(`ielts_assembly_${examId}`) ?? "manual"; }, [examId]); const filters: ContentPoolFilters = useMemo( () => ({ skill, part, difficulty, topics: topicInput .split(",") .map((t) => t.trim()) .filter(Boolean), search: search.trim() || undefined, }), [skill, part, difficulty, topicInput, search], ); const poolQ = useIeltsContentPool(Number.isFinite(examId) ? examId : 0, filters); const skillsQ = useIeltsSkills(Number.isFinite(examId) ? examId : 0); const autoAssemble = useIeltsAutoAssemble(); const items = useMemo(() => { const raw = poolQ.data; if (Array.isArray(raw)) return raw; if (raw && typeof raw === "object" && Array.isArray((raw as { data?: unknown }).data)) { return (raw as { data: ContentPoolItem[] }).data; } return [] as ContentPoolItem[]; }, [poolQ.data]); const skillsData = useMemo(() => { const raw = skillsQ.data; if (Array.isArray(raw)) return raw; if (raw && typeof raw === "object" && Array.isArray((raw as { data?: unknown }).data)) { return (raw as { data: IELTSSkillConfig[] }).data; } return []; }, [skillsQ.data]); const requiredTotal = useMemo(() => { const cfg = skillsData.find((s) => s.skill === skill); if (!cfg?.parts?.length) { if (skill === "listening") return 40; if (skill === "reading") return 40; if (skill === "writing") return 2; return 3; } return cfg.parts.reduce((a, p) => a + p.question_count, 0); }, [skillsData, skill]); useEffect(() => { setSearchParams( (prev) => { const sp = new URLSearchParams(prev); sp.set("skill", skill); sp.set("part", String(part)); return sp; }, { replace: true }, ); }, [skill, part, setSearchParams]); const didAutoRun = useRef(false); useEffect(() => { if (assemblyMode !== "auto" || !Number.isFinite(examId) || didAutoRun.current) return; didAutoRun.current = true; let cancelled = false; (async () => { try { const res = await autoAssemble.mutateAsync(examId); if (cancelled) return; const flat = (Array.isArray(res) ? res : []).flatMap((r) => r.selected_items?.map((i) => i.id) ?? []); setSelected(flat); } catch { toast.error("Auto-assembly could not run."); } })(); return () => { cancelled = true; }; }, [assemblyMode, autoAssemble, examId]); useEffect(() => { if (assemblyMode !== "hybrid" || !Number.isFinite(examId)) return; let cancelled = false; (async () => { try { const res = await ieltsExamService.suggest(examId); if (cancelled) return; const rows = Array.isArray(res) ? res : []; const forSkill = rows.find((r) => r.skill === skill && r.part === part); setPendingHybrid(forSkill?.selected_items ?? []); } catch { setPendingHybrid([]); } })(); return () => { cancelled = true; }; }, [assemblyMode, examId, part, skill]); const toggleSelect = useCallback( (id: number) => { setSelected((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id])); }, [], ); const selectedItems = useMemo(() => { const map = new Map(items.map((i) => [i.id, i])); return selected.map((id) => map.get(id)).filter(Boolean) as ContentPoolItem[]; }, [items, selected]); const poolDisabled = assemblyMode === "auto"; const saveContinue = async () => { try { await ieltsExamService.saveQuestions(examId, part, selected); toast.success("Selections saved."); navigate(`/admin/exam/ielts/${examId}/validate`); } catch { toast.error("Save failed."); } }; if (!Number.isFinite(examId) || examId <= 0) { return

Invalid exam.

; } return (

Content pool

Assembly: {assemblyMode} {assemblyMode === "auto" ? " — pool is read-only; system selections apply." : null} {assemblyMode === "hybrid" ? " — review suggestions on the right." : null}

Browse pool Filter and preview candidate items.
setDifficultyIdx(v[0] ?? 1)} />
setTopicInput(e.target.value)} />
setSearch(e.target.value)} />
{poolQ.isLoading ? (
) : (
{items.map((item) => (
{difficultyLabel(item.difficulty)} Used {item.usage_count}×
{item.preview_text}
{item.topic_tags.map((t) => ( {t} ))}
))} {items.length === 0 ?

No items match these filters.

: null}
)}
Selected questions {selected.length}/{requiredTotal} selected
    {selectedItems.map((i) => (
  • {i.preview_text}
  • ))} {selectedItems.length === 0 ?
  • Nothing selected yet.
  • : null}
{assemblyMode === "hybrid" && pendingHybrid.length > 0 ? (

Suggestions

{pendingHybrid.map((item) => (
{item.preview_text}
))}
) : null}
); }