From 55aede97edfaf78646e95265f2b794e8fe510721 Mon Sep 17 00:00:00 2001 From: Nurfog Date: Fri, 16 Jan 2026 16:29:15 -0300 Subject: [PATCH] feat: Implement quiz attempt tracking and limits with persistence, and add background transcription for lessons. --- services/cms-service/src/handlers.rs | 12 ++++- .../courses/[id]/lessons/[lessonId]/page.tsx | 35 +++++++++++++- .../src/components/blocks/MediaPlayer.tsx | 6 +++ .../src/components/blocks/QuizPlayer.tsx | 48 +++++++++++++++---- 4 files changed, 90 insertions(+), 11 deletions(-) diff --git a/services/cms-service/src/handlers.rs b/services/cms-service/src/handlers.rs index 83f41a5..a5f4fb6 100644 --- a/services/cms-service/src/handlers.rs +++ b/services/cms-service/src/handlers.rs @@ -594,6 +594,14 @@ pub async fn process_transcription( ) .await; + // 4. Spawn background task + let pool_clone = pool.clone(); + tokio::spawn(async move { + if let Err(e) = run_transcription_task(pool_clone, id).await { + tracing::error!("Transcription task failed for lesson {}: {}", id, e); + } + }); + Ok(Json(updated_lesson)) } @@ -1842,7 +1850,7 @@ pub async fn delete_module( return Err(StatusCode::NOT_FOUND); } - Ok(StatusCode::OK) + Ok(StatusCode::NO_CONTENT) } pub async fn delete_lesson( @@ -1897,7 +1905,7 @@ pub async fn delete_lesson( return Err(StatusCode::NOT_FOUND); } - Ok(StatusCode::OK) + Ok(StatusCode::NO_CONTENT) } // User Management diff --git a/web/experience/src/app/courses/[id]/lessons/[lessonId]/page.tsx b/web/experience/src/app/courses/[id]/lessons/[lessonId]/page.tsx index c928f2f..d67a5b9 100644 --- a/web/experience/src/app/courses/[id]/lessons/[lessonId]/page.tsx +++ b/web/experience/src/app/courses/[id]/lessons/[lessonId]/page.tsx @@ -189,7 +189,40 @@ export default function LessonPlayerPage({ params }: { params: { id: string, les /> )} {block.type === 'quiz' && ( - + )[block.id] || 0 + : 0 + } + onAttempt={async () => { + if (user) { + const currentAttempts = (userGrade?.metadata?.block_attempts as Record) || {}; + const newAttempts = { + ...currentAttempts, + [block.id]: (currentAttempts[block.id] || 0) + 1 + }; + + try { + const res = await lmsApi.submitScore( + user.id, + params.id, + params.lessonId, + userGrade?.score || 0, + { ...userGrade?.metadata, block_attempts: newAttempts } + ); + setUserGrade(res); + } catch (err) { + console.error("Failed to persist block attempts", err); + } + } + }} + /> )} {block.type === 'fill-in-the-blanks' && ( diff --git a/web/experience/src/components/blocks/MediaPlayer.tsx b/web/experience/src/components/blocks/MediaPlayer.tsx index ade8c53..41f7a11 100644 --- a/web/experience/src/components/blocks/MediaPlayer.tsx +++ b/web/experience/src/components/blocks/MediaPlayer.tsx @@ -21,6 +21,12 @@ export default function MediaPlayer({ id, title, url, media_type, config, initia const [hasStarted, setHasStarted] = useState(false); const [locked, setLocked] = useState(false); + useEffect(() => { + if (initialPlayCount !== undefined) { + setPlayCount(initialPlayCount); + } + }, [initialPlayCount]); + const maxPlays = config?.maxPlays || 0; const CMS_API_URL = process.env.NEXT_PUBLIC_CMS_API_URL || "http://localhost:3001"; diff --git a/web/experience/src/components/blocks/QuizPlayer.tsx b/web/experience/src/components/blocks/QuizPlayer.tsx index a6a2b33..0e0f5e2 100644 --- a/web/experience/src/components/blocks/QuizPlayer.tsx +++ b/web/experience/src/components/blocks/QuizPlayer.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; interface QuizQuestion { id: string; @@ -17,14 +17,25 @@ interface QuizPlayerProps { questions: QuizQuestion[]; }; allowRetry?: boolean; + maxAttempts?: number; + initialAttempts?: number; + onAttempt?: () => void; } -export default function QuizPlayer({ id, title, quizData, allowRetry = true }: QuizPlayerProps) { +export default function QuizPlayer({ id, title, quizData, allowRetry = true, maxAttempts = 0, initialAttempts = 0, onAttempt }: QuizPlayerProps) { const [userAnswers, setUserAnswers] = useState>({}); const [submitted, setSubmitted] = useState(false); + const [attempts, setAttempts] = useState(initialAttempts || 0); const questions = quizData?.questions || []; + // Sync attempts with prop + useEffect(() => { + if (initialAttempts !== undefined) { + setAttempts(initialAttempts); + } + }, [initialAttempts]); + const handleAnswer = (qId: string, optionIndex: number, isMulti: boolean) => { if (submitted) return; setUserAnswers(prev => { @@ -40,12 +51,27 @@ export default function QuizPlayer({ id, title, quizData, allowRetry = true }: Q }); }; + const handleValidate = () => { + if (maxAttempts > 0 && attempts >= maxAttempts) return; + + setSubmitted(true); + if (onAttempt) { + onAttempt(); + setAttempts(prev => prev + 1); + } + }; + return (

{title || "Knowledge Check"}

+ {maxAttempts > 0 && ( + + Attempt {attempts} / {maxAttempts} + + )}
@@ -93,18 +119,24 @@ export default function QuizPlayer({ id, title, quizData, allowRetry = true }: Q <> {!submitted && questions.length > 0 && ( )} {submitted && ( )}