From bebab7ba1ec95ca1658c1a15a619d7b49d991bdc Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Thu, 16 Apr 2026 04:03:12 -0700 Subject: [PATCH] Add lesson question bank editing For lessons, fetch related questions by lesson question_set_id and render a styled question bank panel (same UX as practice), including add/edit/delete with proper refresh. Made-with: Cursor --- .../content-management/HumanLanguagePage.tsx | 315 +++++++++++++++++- 1 file changed, 304 insertions(+), 11 deletions(-) diff --git a/src/pages/content-management/HumanLanguagePage.tsx b/src/pages/content-management/HumanLanguagePage.tsx index ba9b3a1..3419790 100644 --- a/src/pages/content-management/HumanLanguagePage.tsx +++ b/src/pages/content-management/HumanLanguagePage.tsx @@ -47,6 +47,7 @@ import { getPracticeQuestions, getPracticeQuestionsByPractice, getQuestionSetById, + getQuestionSetQuestions, getSubModuleLessonById, updateQuestionSet, updateQuestion, @@ -123,6 +124,7 @@ type QuestionDialogState = open: true mode: "create" | "edit" practiceId: number + target: "practice" | "lesson" questionId?: number } @@ -364,6 +366,7 @@ export function HumanLanguagePage() { /** Selected lesson / practice card per sub-module (for inline detail panel). */ const [subModuleCardSelection, setSubModuleCardSelection] = useState>({}) const [practiceQuestionsState, setPracticeQuestionsState] = useState>({}) + const [lessonQuestionsState, setLessonQuestionsState] = useState>({}) const [lessonDetailState, setLessonDetailState] = useState>({}) const [practiceDialog, setPracticeDialog] = useState({ open: false }) const [lessonDialog, setLessonDialog] = useState({ open: false }) @@ -393,7 +396,12 @@ export function HumanLanguagePage() { subModuleKey: string } | null>(null) const [selectedLessonIdsBySubModule, setSelectedLessonIdsBySubModule] = useState>({}) - const [questionTargetDelete, setQuestionTargetDelete] = useState<{ id: number; practiceId: number; text: string } | null>(null) + const [questionTargetDelete, setQuestionTargetDelete] = useState<{ + id: number + practiceId: number + text: string + target: "practice" | "lesson" + } | null>(null) const [savingPractice, setSavingPractice] = useState(false) const [savingLesson, setSavingLesson] = useState(false) const [savingQuestion, setSavingQuestion] = useState(false) @@ -888,6 +896,38 @@ export function HumanLanguagePage() { } } + const loadLessonQuestionsIfNeeded = async (questionSetId: number, forceRefresh = false) => { + let skipFetch = false + setLessonQuestionsState((prev) => { + const ex = prev[questionSetId] + if (!forceRefresh && ex?.status === "ok") { + skipFetch = true + return prev + } + if (!forceRefresh && ex?.status === "loading" && Date.now() - ex.startedAt < 15000) { + skipFetch = true + return prev + } + return { ...prev, [questionSetId]: { status: "loading", startedAt: Date.now() } } + }) + if (skipFetch) return + + try { + const res = await withTimeout(getQuestionSetQuestions(questionSetId), 12000) + const questions = res.data?.data ?? [] + setLessonQuestionsState((prev) => ({ + ...prev, + [questionSetId]: { status: "ok", questions, totalCount: questions.length }, + })) + } catch (error) { + console.error("Failed to load lesson questions:", error) + setLessonQuestionsState((prev) => ({ + ...prev, + [questionSetId]: { status: "error", message: "Could not load lesson questions" }, + })) + } + } + const loadLessonDetailIfNeeded = async (lessonId: number, forceRefresh = false) => { let skipFetch = false setLessonDetailState((prev) => { @@ -1103,14 +1143,18 @@ export function HumanLanguagePage() { } } - const openCreateQuestionDialog = (practiceId: number) => { + const openCreateQuestionDialog = (practiceId: number, target: "practice" | "lesson") => { setQuestionSubmitAttempted(false) setQuestionFormTouched(false) resetQuestionForm() - setQuestionDialog({ open: true, mode: "create", practiceId }) + setQuestionDialog({ open: true, mode: "create", practiceId, target }) } - const openEditQuestionDialog = async (practiceId: number, question: QuestionSetQuestion) => { + const openEditQuestionDialog = async ( + practiceId: number, + question: QuestionSetQuestion, + target: "practice" | "lesson", + ) => { setQuestionSubmitAttempted(false) setQuestionFormTouched(false) const qid = question.question_id ?? question.id @@ -1183,7 +1227,7 @@ export function HumanLanguagePage() { imageUrl: detail.image_url ?? "", }) // Open only after the same form shape as create is fully populated (no empty-state flash). - setQuestionDialog({ open: true, mode: "edit", practiceId, questionId: qid }) + setQuestionDialog({ open: true, mode: "edit", practiceId, questionId: qid, target }) } catch (error) { console.error("Failed to load question detail:", error) toast.error("Could not load question details") @@ -1287,7 +1331,9 @@ export function HumanLanguagePage() { setQuestionFormTouched(false) resetQuestionForm() await Promise.all([ - loadPracticeQuestionsIfNeeded(questionDialog.practiceId, true), + questionDialog.target === "practice" + ? loadPracticeQuestionsIfNeeded(questionDialog.practiceId, true) + : loadLessonQuestionsIfNeeded(questionDialog.practiceId, true), loadHierarchy(), ]) } catch (error) { @@ -1356,7 +1402,9 @@ export function HumanLanguagePage() { await deleteQuestion(questionTargetDelete.id) toast.success("Question deleted") await Promise.all([ - loadPracticeQuestionsIfNeeded(questionTargetDelete.practiceId, true), + questionTargetDelete.target === "practice" + ? loadPracticeQuestionsIfNeeded(questionTargetDelete.practiceId, true) + : loadLessonQuestionsIfNeeded(questionTargetDelete.practiceId, true), loadHierarchy(), ]) setQuestionTargetDelete(null) @@ -1368,7 +1416,7 @@ export function HumanLanguagePage() { } } - const toggleLessonCard = (smKey: string, lessonId: number) => { + const toggleLessonCard = (smKey: string, lessonId: number, lessonQuestionSetId: number) => { const currentLessonId = subModuleCardSelection[smKey]?.lessonId ?? null const nextLessonId = currentLessonId === lessonId ? null : lessonId setSubModuleCardSelection((prev) => { @@ -1376,6 +1424,7 @@ export function HumanLanguagePage() { return { ...prev, [smKey]: { ...cur, lessonId: nextLessonId } } }) if (nextLessonId !== null) void loadLessonDetailIfNeeded(nextLessonId) + if (nextLessonId !== null && lessonQuestionSetId > 0) void loadLessonQuestionsIfNeeded(lessonQuestionSetId) } const toggleLessonSelection = (smKey: string, lessonId: number) => { @@ -1797,6 +1846,10 @@ export function HumanLanguagePage() { cardSel.lessonId !== null ? lessonDetailState[cardSel.lessonId] : undefined const selectedLessonDetail = selectedLessonFetch?.status === "ok" ? selectedLessonFetch.data : null + const selectedLessonQuestionSetId = + selectedLessonDetail?.question_set_id ?? selectedLesson?.question_set_id ?? 0 + const lessonFetch = + selectedLessonQuestionSetId > 0 ? lessonQuestionsState[selectedLessonQuestionSetId] : undefined const selectedLessonIds = selectedLessonIdsBySubModule[smKey] ?? [] const selectedLessonRows = lessonRows.filter((row) => selectedLessonIds.includes(row.id)) const selectedPracticeMeta = @@ -1957,7 +2010,7 @@ export function HumanLanguagePage() { + {lessonFetch?.status === "ok" ? ( + + {lessonFetch.questions.length} loaded + + ) : null} + + + +
+ {!lessonFetch || lessonFetch.status === "loading" ? ( +
+ + Loading questions… +
+ ) : null} + {lessonFetch?.status === "error" ? ( +
+
+ +
+

{lessonFetch.message}

+ +
+
+
+ ) : null} + {lessonFetch?.status === "ok" && lessonFetch.questions.length === 0 ? ( +
+ +

No questions in this lesson yet.

+

+ Add them via Open editor. +

+
+ ) : null} + {lessonFetch?.status === "ok" && lessonFetch.questions.length > 0 ? ( +
    + {lessonFetch.questions.map((q, qIdx) => { + const qType = String(q.question_type ?? "—") + const embeddedUrls = extractUrls(q.question_text || "") + return ( +
  • +
    +
    +
    + {qIdx + 1} +
    +
    +
    +
    + + {formatQuestionTypeLabel(qType)} + + {q.points != null && q.points > 0 ? ( + + {q.points} pts + + ) : null} + {q.difficulty_level ? ( + + {q.difficulty_level} + + ) : null} +
    +
    + + +
    +
    +
    +

    + Prompt +

    +

    + {q.question_text?.trim() || ( + No prompt text + )} +

    +
    + {embeddedUrls.length > 0 ? ( +
    +

    + + Media in prompt +

    +
    + {embeddedUrls.map((u) => ( +
    + {renderMediaPreview(u, undefined, "", "Embedded link")} +
    + ))} +
    +
    + ) : null} + {q.tips ? ( +
    +

    + + Learner tip +

    +

    {q.tips}

    +
    + ) : null} + {q.image_url || + q.voice_prompt || + q.sample_answer_voice_prompt ? ( +
    +

    + Assets +

    +
    + {q.image_url + ? renderMediaPreview(q.image_url, "image", "", "Image") + : null} + {q.voice_prompt + ? renderMediaPreview(q.voice_prompt, "audio", "", "Voice prompt") + : null} + {q.sample_answer_voice_prompt + ? renderMediaPreview( + q.sample_answer_voice_prompt, + "audio", + "", + "Sample answer (audio)", + ) + : null} +
    +
    + ) : null} + {q.audio_correct_answer_text ? ( +
    +

    + Sample answer text +

    +

    + {q.audio_correct_answer_text} +

    +
    + ) : null} +
    +
    +
  • + ) + })} +
+ ) : null} +
+ + )} + ) : (

Select a lesson card to view full content. @@ -2202,7 +2493,7 @@ export function HumanLanguagePage() { size="sm" variant="outline" className="h-8 text-xs" - onClick={() => openCreateQuestionDialog(selectedPracticeMeta.id)} + onClick={() => openCreateQuestionDialog(selectedPracticeMeta.id, "practice")} > Add question @@ -2306,6 +2597,7 @@ export function HumanLanguagePage() { void openEditQuestionDialog( selectedPracticeMeta.id, q, + "practice", ) } > @@ -2325,6 +2617,7 @@ export function HumanLanguagePage() { id: q.question_id ?? q.id, practiceId: selectedPracticeMeta.id, text: q.question_text || "Question", + target: "practice", }) } >