From b11cfe1cdd952ec99dd2159271e16e9266b98fd2 Mon Sep 17 00:00:00 2001 From: ngochoa203 Date: Fri, 9 Jan 2026 15:09:14 +0700 Subject: [PATCH 1/9] fix --- services/course-service/src/controllers/course_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/course-service/src/controllers/course_controller.py b/services/course-service/src/controllers/course_controller.py index 742ae055..9fdcc87e 100644 --- a/services/course-service/src/controllers/course_controller.py +++ b/services/course-service/src/controllers/course_controller.py @@ -1391,7 +1391,7 @@ def submit_assessment_controller(request: Request, course_id: str, lesson_id: st continue selected = int(ans.answer) db_answer = cast(int, getattr(qa, "answer")) - correct = (db_answer % 4) + 1 + correct = db_answer is_correct = selected == correct if is_correct: correct_count += 1 From 944e566418880eabc1c74b17e195083eb5061e67 Mon Sep 17 00:00:00 2001 From: ngochoa203 Date: Fri, 9 Jan 2026 15:56:06 +0700 Subject: [PATCH 2/9] fix --- .../components/user/chatbot/LessonChatbot.tsx | 33 +++++++++++++++++-- .../src/controllers/chatbot_controller.py | 20 +++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/user/chatbot/LessonChatbot.tsx b/frontend/src/components/user/chatbot/LessonChatbot.tsx index 0206c407..b5e1fa6a 100644 --- a/frontend/src/components/user/chatbot/LessonChatbot.tsx +++ b/frontend/src/components/user/chatbot/LessonChatbot.tsx @@ -23,6 +23,7 @@ export default function LessonChatbot({ lessonId, courseId }: ChatbotProps) { const [inputValue, setInputValue] = useState(''); const [isLoading, setIsLoading] = useState(false); const [pendingChatId, setPendingChatId] = useState(null); + const [pollStartTime, setPollStartTime] = useState(null); const messagesEndRef = useRef(null); const lastActivityRef = useRef(Date.now()); @@ -73,8 +74,27 @@ export default function LessonChatbot({ lessonId, courseId }: ChatbotProps) { useEffect(() => { if (!pendingChatId) return; + const POLLING_TIMEOUT = 180000; // 3 minutes (longer timeout for when course generation is running) + const pollAnswer = async () => { try { + // Check timeout + if (pollStartTime && Date.now() - pollStartTime > POLLING_TIMEOUT) { + setMessages(prev => [ + ...prev, + { + id: pendingChatId, + role: 'assistant', + content: 'Xin lỗi, hệ thống đang bận xử lý. Câu hỏi này có thể mất vài phút để trả lời. Bạn vui lòng thử lại sau nhé! 🙏', + timestamp: new Date(), + }, + ]); + setPendingChatId(null); + setIsLoading(false); + setPollStartTime(null); + return; + } + const response = await chatbotApi.getAnswer(pendingChatId); const data = response.data; @@ -90,6 +110,7 @@ export default function LessonChatbot({ lessonId, courseId }: ChatbotProps) { ]); setPendingChatId(null); setIsLoading(false); + setPollStartTime(null); } else if (data.status === 500) { const errorMessage = data.message || 'Xin lỗi, mình gặp lỗi khi xử lý câu hỏi của bạn. Bạn có thể thử lại không?'; setMessages(prev => [ @@ -103,11 +124,18 @@ export default function LessonChatbot({ lessonId, courseId }: ChatbotProps) { ]); setPendingChatId(null); setIsLoading(false); + setPollStartTime(null); + } + // For 202 (processing) or 404 (not yet created), continue polling + } catch (error: any) { + // If 404, treat as still processing (Lambda hasn't created record yet) + if (error?.response?.status === 404) { + return; // Continue polling } - } catch (error) { console.error('Error polling answer:', error); setPendingChatId(null); setIsLoading(false); + setPollStartTime(null); } }; @@ -116,10 +144,11 @@ export default function LessonChatbot({ lessonId, courseId }: ChatbotProps) { return () => { if (pollingIntervalRef.current) { + setPollStartTime(Date.now()); // Start timeout timer clearInterval(pollingIntervalRef.current); } }; - }, [pendingChatId]); + }, [pendingChatId, pollStartTime]); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); diff --git a/services/course-service/src/controllers/chatbot_controller.py b/services/course-service/src/controllers/chatbot_controller.py index 9ce2a464..350d44cb 100644 --- a/services/course-service/src/controllers/chatbot_controller.py +++ b/services/course-service/src/controllers/chatbot_controller.py @@ -42,6 +42,26 @@ def submit_question( chat_id = request.chat_id or str(uuid.uuid4()) try: + # Create placeholder record in DynamoDB immediately + dynamodb = _get_dynamodb_client() + table_name = _get_chat_table_name() + now = datetime.now(timezone.utc).isoformat() + + dynamodb.put_item( + TableName=table_name, + Item={ + "chat_id": {"S": chat_id}, + "user_id": {"S": user_id}, + "message": {"S": request.message}, + "lesson_id": {"S": request.lesson_id or ""}, + "course_id": {"S": request.course_id or ""}, + "status": {"S": "processing"}, + "created_at": {"S": now}, + "updated_at": {"S": now}, + } + ) + + # Send to SQS for async processing send_chatbot_question( queue_url=self.sqs_queue_url, chat_id=chat_id, From ce4eb360e2cfdd794c48d9a9a7f9199d8945e514 Mon Sep 17 00:00:00 2001 From: ngochoa203 Date: Fri, 9 Jan 2026 16:02:44 +0700 Subject: [PATCH 3/9] fix --- .../src/controllers/chatbot_controller.py | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/services/course-service/src/controllers/chatbot_controller.py b/services/course-service/src/controllers/chatbot_controller.py index 350d44cb..2f4b8e2d 100644 --- a/services/course-service/src/controllers/chatbot_controller.py +++ b/services/course-service/src/controllers/chatbot_controller.py @@ -42,14 +42,14 @@ def submit_question( chat_id = request.chat_id or str(uuid.uuid4()) try: - # Create placeholder record in DynamoDB immediately + # Create placeholder record in DynamoDB immediately to prevent 404 dynamodb = _get_dynamodb_client() table_name = _get_chat_table_name() now = datetime.now(timezone.utc).isoformat() - dynamodb.put_item( - TableName=table_name, - Item={ + try: + # Create comprehensive placeholder to prevent Lambda overwrite + item = { "chat_id": {"S": chat_id}, "user_id": {"S": user_id}, "message": {"S": request.message}, @@ -58,8 +58,22 @@ def submit_question( "status": {"S": "processing"}, "created_at": {"S": now}, "updated_at": {"S": now}, + "response": {"S": ""}, # Empty response initially + "error_message": {"S": ""}, } - ) + + # Add context_chunks as empty list + item["context_chunks"] = {"L": []} + + print(f"Creating placeholder record for chat_id: {chat_id} in table: {table_name}") + dynamodb.put_item( + TableName=table_name, + Item=item + ) + print(f"Placeholder created successfully for chat_id: {chat_id}") + except ClientError as db_error: + # Log but don't fail - Lambda will create it later + print(f"Warning: Failed to create placeholder record for {chat_id}: {db_error}") # Send to SQS for async processing send_chatbot_question( From 1b28f2dbbce6f0b1ed2d5aacc6ebcea46a5a05ce Mon Sep 17 00:00:00 2001 From: ngochoa203 Date: Fri, 9 Jan 2026 16:09:26 +0700 Subject: [PATCH 4/9] fix --- frontend/src/app/user/my-courses/page.tsx | 3 ++- frontend/src/app/user/my-quizzes/[quizId]/play/page.tsx | 6 ++++-- frontend/src/app/user/ranking/page.tsx | 2 +- frontend/src/components/user/chatbot/LessonChatbot.tsx | 9 ++++++--- .../src/components/user/create-course/SuccessStep.tsx | 3 ++- .../course-service/src/controllers/chatbot_controller.py | 4 ++-- 6 files changed, 17 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/user/my-courses/page.tsx b/frontend/src/app/user/my-courses/page.tsx index 5817d241..db6f19f8 100644 --- a/frontend/src/app/user/my-courses/page.tsx +++ b/frontend/src/app/user/my-courses/page.tsx @@ -175,7 +175,8 @@ function MyCoursesContent() { return () => { cancelled = true; }; - }, [authLoading, isAuthenticated]); // Removed selectedId to prevent infinite loop + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [authLoading, isAuthenticated]); // selectedId intentionally omitted to prevent infinite loop // Load recommended courses immediately on mount useEffect(() => { diff --git a/frontend/src/app/user/my-quizzes/[quizId]/play/page.tsx b/frontend/src/app/user/my-quizzes/[quizId]/play/page.tsx index 7b94b34e..2c0d8e3c 100644 --- a/frontend/src/app/user/my-quizzes/[quizId]/play/page.tsx +++ b/frontend/src/app/user/my-quizzes/[quizId]/play/page.tsx @@ -227,7 +227,8 @@ export default function QuizPlayPage({ params }: PageProps) { }; load(); return () => { cancelled = true; }; - }, [quizId]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [quizId]); // answerDisplayMode intentionally omitted const handleTimeUp = useCallback(() => { // Prevent duplicate execution @@ -269,7 +270,8 @@ export default function QuizPlayPage({ params }: PageProps) { // Reset flag after transition isProcessingTimeout.current = false; }, 1500); - }, [quiz, currentIndex, answers, timePerQuestion]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [quiz, currentIndex, answers, timePerQuestion]); // handleSubmit intentionally omitted to avoid circular dependency // Timer countdown useEffect(() => { diff --git a/frontend/src/app/user/ranking/page.tsx b/frontend/src/app/user/ranking/page.tsx index b7d87a0a..43813e11 100644 --- a/frontend/src/app/user/ranking/page.tsx +++ b/frontend/src/app/user/ranking/page.tsx @@ -11,7 +11,7 @@ export default function RankingPage() { const handleLogout = useCallback(() => router.push('/auth/login'), [router]); const { user, dashboardData, loading } = useDashboard(handleLogout); - const users = dashboardData?.info?.user_top_rank || []; + const users = useMemo(() => dashboardData?.info?.user_top_rank || [], [dashboardData]); // Find competitors (user + 1 above + 1 below) const competitors = useMemo(() => { diff --git a/frontend/src/components/user/chatbot/LessonChatbot.tsx b/frontend/src/components/user/chatbot/LessonChatbot.tsx index b5e1fa6a..aa9b5a97 100644 --- a/frontend/src/components/user/chatbot/LessonChatbot.tsx +++ b/frontend/src/components/user/chatbot/LessonChatbot.tsx @@ -127,10 +127,13 @@ export default function LessonChatbot({ lessonId, courseId }: ChatbotProps) { setPollStartTime(null); } // For 202 (processing) or 404 (not yet created), continue polling - } catch (error: any) { + } catch (error: unknown) { // If 404, treat as still processing (Lambda hasn't created record yet) - if (error?.response?.status === 404) { - return; // Continue polling + if (error && typeof error === 'object' && 'response' in error) { + const err = error as { response?: { status?: number } }; + if (err.response?.status === 404) { + return; // Continue polling + } } console.error('Error polling answer:', error); setPendingChatId(null); diff --git a/frontend/src/components/user/create-course/SuccessStep.tsx b/frontend/src/components/user/create-course/SuccessStep.tsx index 56ca5292..12098ca1 100644 --- a/frontend/src/components/user/create-course/SuccessStep.tsx +++ b/frontend/src/components/user/create-course/SuccessStep.tsx @@ -144,7 +144,8 @@ export function SuccessStep({ draft, result, onRestart, onGoToCourses }: Success } else { setIsPolling(false); } - }, [courseId, status.overallStatus]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [courseId, status.overallStatus]); // fetchGenerationStatus intentionally omitted to prevent re-creation loop const renderContent = (content: string) => { diff --git a/services/course-service/src/controllers/chatbot_controller.py b/services/course-service/src/controllers/chatbot_controller.py index 2f4b8e2d..cdd40641 100644 --- a/services/course-service/src/controllers/chatbot_controller.py +++ b/services/course-service/src/controllers/chatbot_controller.py @@ -62,8 +62,8 @@ def submit_question( "error_message": {"S": ""}, } - # Add context_chunks as empty list - item["context_chunks"] = {"L": []} + # Add context_chunks as empty list in DynamoDB attribute value format + item["context_chunks"] = {"L": []} # type: ignore print(f"Creating placeholder record for chat_id: {chat_id} in table: {table_name}") dynamodb.put_item( From b3eb4829eaa2a0c1f23d89aec1dbe23127eacee8 Mon Sep 17 00:00:00 2001 From: ngochoa203 Date: Fri, 9 Jan 2026 17:19:10 +0700 Subject: [PATCH 5/9] fix --- .../user/generation/GenerationCard.tsx | 89 ++++++----- .../user/generation/GenerationDetailModal.tsx | 105 +++++++++---- .../src/services/status_service.py | 145 ++++++++++++++---- 3 files changed, 240 insertions(+), 99 deletions(-) diff --git a/frontend/src/components/user/generation/GenerationCard.tsx b/frontend/src/components/user/generation/GenerationCard.tsx index 8b7dfb25..eff28975 100644 --- a/frontend/src/components/user/generation/GenerationCard.tsx +++ b/frontend/src/components/user/generation/GenerationCard.tsx @@ -6,52 +6,69 @@ import { CheckCircle, Clock, Zap, FileText, User, CalendarClock } from "lucide-r export type GenerationItem = { course_id: string; user_id?: string; + overall_status?: 'processing' | 'done' | 'error'; + error_message?: string; + title?: string; description?: string; - progress?: string; - title_ready?: boolean; - lessons_ready?: boolean; - tests_ready?: boolean; - final_ready?: boolean; - vectorized?: boolean; - lessons_count?: number; - lessons_planned?: number; - roadmap_count?: number; - tests_count?: number; - final_count?: number; - updated_at?: string; + vectorization_status?: string; + vectorization_chunks?: number; + + planning_status?: string; + planning_course_title?: string; + planning_roadmap_count?: number; + + lessons_status?: string; + lessons_completed?: number; + lessons_total?: number; + lessons_list?: Array<{ id: string; title: string }>; + + tests_status?: string; + tests_completed?: number; + tests_total?: number; + + start_timestamp?: number; + last_updated?: string; + end_timestamp?: number; }; function getGenerationStatus(item: GenerationItem) { const steps = [ - { key: 'vectorized', label: 'Vectorized', done: Boolean(item.vectorized), count: null }, - { key: 'title_ready', label: 'Plan', done: Boolean(item.title_ready), count: item.roadmap_count }, - { key: 'lessons_ready', label: 'Lessons', done: Boolean(item.lessons_ready), count: item.lessons_count }, - { key: 'tests_ready', label: 'Tests', done: Boolean(item.tests_ready), count: item.tests_count }, + { + key: 'vectorization', + label: 'Vectorized', + done: item.vectorization_status === 'done', + count: item.vectorization_chunks + }, + { + key: 'planning', + label: 'Plan', + done: item.planning_status === 'done', + count: item.planning_roadmap_count + }, + { + key: 'lessons', + label: 'Lessons', + done: item.lessons_status === 'done', + count: item.lessons_completed && item.lessons_total ? `${item.lessons_completed}/${item.lessons_total}` : item.lessons_total + }, + { + key: 'tests', + label: 'Tests', + done: item.tests_status === 'done', + count: item.tests_completed && item.tests_total ? `${item.tests_completed}/${item.tests_total}` : item.tests_total + }, ]; const completedSteps = steps.filter(s => s.done).length; - const isComplete = completedSteps === steps.length; + const isComplete = item.overall_status === 'done'; const currentStep = steps.find(s => !s.done); const progressPercent = (completedSteps / steps.length) * 100; - - // Use final_ready as overall status indicator, but also check if all steps are done - let overallStatus: 'done' | 'processing' | 'error' = 'processing'; - if (isComplete || item.final_ready === true) { - overallStatus = 'done'; - } else if (item.final_ready === false && item.progress?.includes('error')) { - overallStatus = 'error'; - } + const overallStatus = item.overall_status || 'processing'; return { steps, completedSteps, isComplete, currentStep, progressPercent, overallStatus }; } -// Truncate text for display -function truncateText(text: string, maxLength: number) { - if (text.length <= maxLength) return text; - return text.slice(0, maxLength - 3) + "..."; -} - // Format time difference function getTimeElapsed(dateStr?: string): string { if (!dateStr) return "Không rõ"; @@ -96,12 +113,12 @@ export default function GenerationCard({ userNames?: Record; }) { const status = getGenerationStatus(item); - const displayTitle = item.title || "Đang tạo khóa học..."; - const displayDesc = item.description ? truncateText(item.description, 120) : null; - const timeElapsed = getTimeElapsed(item.updated_at); + const displayTitle = item.planning_course_title || "Đang tạo khóa học..."; + const displayDesc = item.error_message || null; + const timeElapsed = getTimeElapsed(item.last_updated); const creatorName = item.user_id && userNames?.[item.user_id] ? userNames[item.user_id] : getUserDisplayName(item.user_id); - const lessonsInfo = item.lessons_planned - ? `${item.lessons_count || 0}/${item.lessons_planned}` + const lessonsInfo = item.lessons_total + ? `${item.lessons_completed || 0}/${item.lessons_total}` : null; return ( diff --git a/frontend/src/components/user/generation/GenerationDetailModal.tsx b/frontend/src/components/user/generation/GenerationDetailModal.tsx index c0996d3e..cf86ebe0 100644 --- a/frontend/src/components/user/generation/GenerationDetailModal.tsx +++ b/frontend/src/components/user/generation/GenerationDetailModal.tsx @@ -1,4 +1,5 @@ "use client"; +import { useState } from 'react'; import { Dialog, DialogContent, @@ -7,53 +8,46 @@ import { DialogDescription, } from "@/components/ui/dialog"; import { Badge } from "@/components/ui/badge"; -import { CheckCircle, Clock, Zap, Calendar, Hash } from "lucide-react"; +import { CheckCircle, Clock, Zap, Calendar, Hash, ChevronDown, ChevronUp, BookOpen } from "lucide-react"; import { GenerationItem } from "./GenerationCard"; function getDetailedStatus(item: GenerationItem) { const steps = [ { - key: 'vectorized', + key: 'vectorization', label: 'Phân tích tài liệu', - done: Boolean(item.vectorized), - description: 'Vectorize và index tài liệu vào OpenSearch', - count: null + done: item.vectorization_status === 'done', + description: `Vectorize và index tài liệu vào OpenSearch${item.vectorization_chunks ? ` (${item.vectorization_chunks} chunks)` : ''}`, + count: item.vectorization_chunks }, { - key: 'title_ready', + key: 'planning', label: 'Tạo kế hoạch khóa học', - done: Boolean(item.title_ready), - description: 'Sinh tiêu đề, mô tả và roadmap học tập', - count: item.roadmap_count + done: item.planning_status === 'done', + description: `Sinh tiêu đề, mô tả và roadmap học tập${item.planning_roadmap_count ? ` (${item.planning_roadmap_count} modules)` : ''}`, + count: item.planning_roadmap_count }, { - key: 'lessons_ready', - label: 'Lập kế hoạch bài học', - done: Boolean(item.lessons_ready), - description: `Xác định số lượng bài học cần tạo${item.lessons_count ? ` (${item.lessons_count} bài)` : ''}`, - count: item.lessons_count + key: 'lessons', + label: 'Tạo nội dung bài học', + done: item.lessons_status === 'done', + description: `Sinh nội dung chi tiết cho từng bài học${item.lessons_completed && item.lessons_total ? ` (${item.lessons_completed}/${item.lessons_total} bài hoàn thành)` : ''}`, + count: item.lessons_completed && item.lessons_total ? `${item.lessons_completed}/${item.lessons_total}` : item.lessons_total }, { - key: 'tests_ready', - label: 'Tạo nội dung bài học', - done: Boolean(item.tests_ready), - description: `Sinh nội dung chi tiết cho từng bài học${item.lessons_count ? ` (${item.lessons_count} bài hoàn thành)` : ''}`, - count: null + key: 'tests', + label: 'Tạo câu hỏi kiểm tra', + done: item.tests_status === 'done', + description: `Sinh câu hỏi trắc nghiệm cho từng bài học${item.tests_completed && item.tests_total ? ` (${item.tests_completed}/${item.tests_total} bài)` : ''}`, + count: item.tests_completed && item.tests_total ? `${item.tests_completed}/${item.tests_total}` : item.tests_total }, ]; const completedSteps = steps.filter(s => s.done).length; - const isComplete = completedSteps === steps.length; + const isComplete = item.overall_status === 'done'; const currentStep = steps.find(s => !s.done); const progressPercent = (completedSteps / steps.length) * 100; - - // Use final_ready as overall status, but also check if all steps are done - let overallStatus: 'done' | 'processing' | 'error' = 'processing'; - if (isComplete || item.final_ready === true) { - overallStatus = 'done'; - } else if (item.final_ready === false && item.progress?.includes('error')) { - overallStatus = 'error'; - } + const overallStatus = item.overall_status || 'processing'; return { steps, completedSteps, isComplete, currentStep, progressPercent, overallStatus }; } @@ -67,10 +61,12 @@ export default function GenerationDetailModal({ isOpen: boolean; onClose: () => void; }) { + const [isLessonsExpanded, setIsLessonsExpanded] = useState(false); + if (!item) return null; const status = getDetailedStatus(item); - const updated = item.updated_at ? new Date(item.updated_at).toLocaleString('vi-VN') : "Chưa có thông tin"; + const updated = item.last_updated ? new Date(item.last_updated).toLocaleString('vi-VN') : "Chưa có thông tin"; return ( @@ -94,9 +90,9 @@ export default function GenerationDetailModal({
{/* Course Info */}
- {item.title && ( + {item.planning_course_title && (
-

{item.title}

+

{item.planning_course_title}

)} {item.description && ( @@ -216,7 +212,9 @@ export default function GenerationDetailModal({

Chi tiết các bước

{status.steps.map((step, index) => { - const isActive = !status.isComplete && status.currentStep?.key === step.key; + const isLessonsStep = step.key === 'lessons'; + const hasLessons = isLessonsStep && item.lessons_list && item.lessons_list.length > 0; + return (
{step.description}

+ + {/* Lessons List Expandable Section */} + {hasLessons && ( +
+ + + {isLessonsExpanded && ( +
+ {item.lessons_list?.map((lesson, idx) => ( +
+
+ {idx + 1} +
+
+

+ {lesson.title} +

+

+ {lesson.id} +

+
+
+ ))} +
+ )} +
+ )}
diff --git a/services/course-service/src/services/status_service.py b/services/course-service/src/services/status_service.py index 4cbd7d9e..f5a10ce4 100644 --- a/services/course-service/src/services/status_service.py +++ b/services/course-service/src/services/status_service.py @@ -60,20 +60,63 @@ def fetch_generation_status(course_id: str) -> Optional[Dict[str, Any]]: item = resp.get("Item") if not item: return None - plan = bool(item.get("title_ready", False)) - lessons = bool(item.get("lessons_ready", False)) - final = bool(item.get("final_ready", False)) - vectorized = bool(item.get("vectorized", False)) - overall = plan and lessons and final + + # Extract status fields + vectorization_status = "done" if bool(item.get("vectorized", False)) else "pending" + planning_status = "done" if bool(item.get("title_ready", False)) else "pending" + lessons_status = "done" if bool(item.get("lessons_ready", False)) else "pending" + tests_status = "done" if bool(item.get("tests_ready", False)) else "pending" + + # Calculate overall status + all_done = ( + vectorization_status == "done" and + planning_status == "done" and + lessons_status == "done" and + tests_status == "done" + ) + overall_status = "done" if all_done else "processing" + if item.get("error_message"): + overall_status = "error" + + # Build lessons list + lessons_list = [] + lessons_completed = 0 + lessons_total = item.get("lessons_count") or item.get("roadmap_count") or 0 + + if item.get("lessons_data"): + for lesson in item.get("lessons_data", []): + lessons_list.append({ + "id": lesson.get("id", ""), + "title": lesson.get("title", "") + }) + if lesson.get("completed"): + lessons_completed += 1 + else: + lessons_completed = lessons_total if lessons_status == "done" else 0 + return { - "status": overall, - "vectorized": vectorized, - "generated_plan": plan, - "generated_lessons": lessons, - "generated_final_test": final, - # Optional extras to help the UI if needed - "progress": item.get("progress"), - "updated_at": item.get("updated_at"), + "overall_status": overall_status, + "error_message": item.get("error_message", ""), + + "vectorization_status": vectorization_status, + "vectorization_chunks": item.get("vectorization_chunks", 0), + + "planning_status": planning_status, + "planning_course_title": item.get("title", ""), + "planning_roadmap_count": item.get("roadmap_count", 0), + + "lessons_status": lessons_status, + "lessons_completed": lessons_completed, + "lessons_total": lessons_total, + "lessons_list": lessons_list, + + "tests_status": tests_status, + "tests_completed": lessons_total if tests_status == "done" else 0, + "tests_total": lessons_total, + + "start_timestamp": item.get("start_timestamp"), + "last_updated": item.get("updated_at"), + "end_timestamp": item.get("end_timestamp"), } except (ClientError, BotoCoreError) as e: logger.error("DynamoDB get_item error: %s", e) @@ -106,25 +149,65 @@ def fetch_user_generations(user_id: str) -> Optional[List[Dict[str, Any]]]: # Normalize a subset of fields for frontend normalized: List[Dict[str, Any]] = [] for it in items: - normalized.append( - { - "course_id": it.get("course_id"), - "user_id": it.get("user_id"), - "title": it.get("title"), - "description": it.get("description"), - "progress": it.get("progress"), - "title_ready": bool(it.get("title_ready", False)), - "lessons_ready": bool(it.get("lessons_ready", False)), - "tests_ready": bool(it.get("tests_ready", False)), - "final_ready": bool(it.get("final_ready", False)), - "vectorized": bool(it.get("vectorized", False)), - "lessons_count": it.get("lessons_count"), - "lessons_planned": it.get("lessons_planned"), - "roadmap_count": it.get("roadmap_count"), - "tests_count": it.get("lessons_count"), - "updated_at": it.get("updated_at"), - } + # Extract status fields + vectorization_status = "done" if bool(it.get("vectorized", False)) else "pending" + planning_status = "done" if bool(it.get("title_ready", False)) else "pending" + lessons_status = "done" if bool(it.get("lessons_ready", False)) else "pending" + tests_status = "done" if bool(it.get("tests_ready", False)) else "pending" + + # Calculate overall status + all_done = ( + vectorization_status == "done" and + planning_status == "done" and + lessons_status == "done" and + tests_status == "done" ) + overall_status = "done" if all_done else "processing" + if it.get("error_message"): + overall_status = "error" + + # Build lessons list + lessons_list = [] + lessons_completed = 0 + lessons_total = it.get("lessons_count") or it.get("roadmap_count") or 0 + + if it.get("lessons_data"): + for lesson in it.get("lessons_data", []): + lessons_list.append({ + "id": lesson.get("id", ""), + "title": lesson.get("title", "") + }) + if lesson.get("completed"): + lessons_completed += 1 + else: + lessons_completed = lessons_total if lessons_status == "done" else 0 + + normalized.append({ + "course_id": it.get("course_id"), + "user_id": it.get("user_id"), + "overall_status": overall_status, + "error_message": it.get("error_message", ""), + + "vectorization_status": vectorization_status, + "vectorization_chunks": it.get("vectorization_chunks", 0), + + "planning_status": planning_status, + "planning_course_title": it.get("title", ""), + "planning_roadmap_count": it.get("roadmap_count", 0), + + "lessons_status": lessons_status, + "lessons_completed": lessons_completed, + "lessons_total": lessons_total, + "lessons_list": lessons_list, + + "tests_status": tests_status, + "tests_completed": lessons_total if tests_status == "done" else 0, + "tests_total": lessons_total, + + "start_timestamp": it.get("start_timestamp"), + "last_updated": it.get("updated_at"), + "end_timestamp": it.get("end_timestamp"), + }) return normalized except (ClientError, BotoCoreError) as e: logger.error("DynamoDB scan error: %s", e) From c5720fc01822483b473507fd7bea3d5ec026303e Mon Sep 17 00:00:00 2001 From: ngochoa203 Date: Fri, 9 Jan 2026 17:24:08 +0700 Subject: [PATCH 6/9] fix --- frontend/src/app/user/tracking/page.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/user/tracking/page.tsx b/frontend/src/app/user/tracking/page.tsx index 3f8be2ff..1349655e 100644 --- a/frontend/src/app/user/tracking/page.tsx +++ b/frontend/src/app/user/tracking/page.tsx @@ -44,9 +44,9 @@ function TrackingInner() { if (resp.status === 200 && Array.isArray(resp.data?.items)) { const items = resp.data.items as GenerationItem[]; items.sort((a, b) => { - if (!a.updated_at) return 1; - if (!b.updated_at) return -1; - return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(); + if (!a.last_updated) return 1; + if (!b.last_updated) return -1; + return new Date(b.last_updated).getTime() - new Date(a.last_updated).getTime(); }); setCourseItems(items); } else if (resp.status === 401) { From e9d1e1168e2f7be05d615fc5019cfa04f85b77c1 Mon Sep 17 00:00:00 2001 From: ngochoa203 Date: Fri, 9 Jan 2026 17:47:12 +0700 Subject: [PATCH 7/9] fix --- frontend/src/app/user/tracking/page.tsx | 8 +++--- .../user/generation/GenerationCard.tsx | 5 ++-- .../user/generation/GenerationDetailModal.tsx | 26 ++++++++----------- .../src/services/status_service.py | 2 ++ 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/frontend/src/app/user/tracking/page.tsx b/frontend/src/app/user/tracking/page.tsx index 1349655e..92782621 100644 --- a/frontend/src/app/user/tracking/page.tsx +++ b/frontend/src/app/user/tracking/page.tsx @@ -44,9 +44,11 @@ function TrackingInner() { if (resp.status === 200 && Array.isArray(resp.data?.items)) { const items = resp.data.items as GenerationItem[]; items.sort((a, b) => { - if (!a.last_updated) return 1; - if (!b.last_updated) return -1; - return new Date(b.last_updated).getTime() - new Date(a.last_updated).getTime(); + const aTime = a.updated_at || a.last_updated; + const bTime = b.updated_at || b.last_updated; + if (!aTime) return 1; + if (!bTime) return -1; + return new Date(bTime).getTime() - new Date(aTime).getTime(); }); setCourseItems(items); } else if (resp.status === 401) { diff --git a/frontend/src/components/user/generation/GenerationCard.tsx b/frontend/src/components/user/generation/GenerationCard.tsx index eff28975..ae8926aa 100644 --- a/frontend/src/components/user/generation/GenerationCard.tsx +++ b/frontend/src/components/user/generation/GenerationCard.tsx @@ -28,6 +28,7 @@ export type GenerationItem = { tests_total?: number; start_timestamp?: number; + updated_at?: string; last_updated?: string; end_timestamp?: number; }; @@ -69,7 +70,7 @@ function getGenerationStatus(item: GenerationItem) { return { steps, completedSteps, isComplete, currentStep, progressPercent, overallStatus }; } -// Format time difference +// Format time difference (prefers updated_at over last_updated) function getTimeElapsed(dateStr?: string): string { if (!dateStr) return "Không rõ"; @@ -115,7 +116,7 @@ export default function GenerationCard({ const status = getGenerationStatus(item); const displayTitle = item.planning_course_title || "Đang tạo khóa học..."; const displayDesc = item.error_message || null; - const timeElapsed = getTimeElapsed(item.last_updated); + const timeElapsed = getTimeElapsed(item.updated_at || item.last_updated); const creatorName = item.user_id && userNames?.[item.user_id] ? userNames[item.user_id] : getUserDisplayName(item.user_id); const lessonsInfo = item.lessons_total ? `${item.lessons_completed || 0}/${item.lessons_total}` diff --git a/frontend/src/components/user/generation/GenerationDetailModal.tsx b/frontend/src/components/user/generation/GenerationDetailModal.tsx index cf86ebe0..e70a51cc 100644 --- a/frontend/src/components/user/generation/GenerationDetailModal.tsx +++ b/frontend/src/components/user/generation/GenerationDetailModal.tsx @@ -66,7 +66,7 @@ export default function GenerationDetailModal({ if (!item) return null; const status = getDetailedStatus(item); - const updated = item.last_updated ? new Date(item.last_updated).toLocaleString('vi-VN') : "Chưa có thông tin"; + const updated = (item.updated_at || item.last_updated) ? new Date(item.updated_at || item.last_updated || '').toLocaleString('vi-VN') : "Chưa có thông tin"; return ( @@ -110,24 +110,24 @@ export default function GenerationDetailModal({ {item.course_id}
- {item.lessons_planned && ( + {item.lessons_total && (
Số bài học: - {item.lessons_count || 0}/{item.lessons_planned} bài + {item.lessons_completed || 0}/{item.lessons_total} bài
)} - {item.roadmap_count && ( + {item.planning_roadmap_count && (
Roadmap: - {item.roadmap_count} bước + {item.planning_roadmap_count} modules
)} - {item.final_count !== undefined && ( + {item.tests_total !== undefined && (
Câu hỏi kiểm tra: - {item.final_count} câu + {item.tests_total} bài
)}
@@ -195,23 +195,19 @@ export default function GenerationDetailModal({

Lỗi xảy ra

Quá trình tạo khóa học gặp sự cố

+ {item.error_message && ( +

“{item.error_message}”

+ )}

Vui lòng thử lại hoặc liên hệ hỗ trợ

)} - {/* Progress Text */} - {item.progress && ( -
-

Trạng thái hiện tại

-

“{item.progress}”

-
- )} - {/* Detailed Steps */}

Chi tiết các bước

{status.steps.map((step, index) => { + const isActive = !status.isComplete && status.currentStep?.key === step.key; const isLessonsStep = step.key === 'lessons'; const hasLessons = isLessonsStep && item.lessons_list && item.lessons_list.length > 0; diff --git a/services/course-service/src/services/status_service.py b/services/course-service/src/services/status_service.py index f5a10ce4..e0d90afb 100644 --- a/services/course-service/src/services/status_service.py +++ b/services/course-service/src/services/status_service.py @@ -115,6 +115,7 @@ def fetch_generation_status(course_id: str) -> Optional[Dict[str, Any]]: "tests_total": lessons_total, "start_timestamp": item.get("start_timestamp"), + "updated_at": item.get("updated_at"), "last_updated": item.get("updated_at"), "end_timestamp": item.get("end_timestamp"), } @@ -205,6 +206,7 @@ def fetch_user_generations(user_id: str) -> Optional[List[Dict[str, Any]]]: "tests_total": lessons_total, "start_timestamp": it.get("start_timestamp"), + "updated_at": it.get("updated_at"), "last_updated": it.get("updated_at"), "end_timestamp": it.get("end_timestamp"), }) From 2ef34698663401c4d4b7bd07c999ef32703708e8 Mon Sep 17 00:00:00 2001 From: ngochoa203 Date: Fri, 9 Jan 2026 18:20:28 +0700 Subject: [PATCH 8/9] fix --- .../user/create-course/SuccessStep.tsx | 167 ++++++++++++------ .../src/services/status_service.py | 118 +++++++------ 2 files changed, 183 insertions(+), 102 deletions(-) diff --git a/frontend/src/components/user/create-course/SuccessStep.tsx b/frontend/src/components/user/create-course/SuccessStep.tsx index 12098ca1..be17377c 100644 --- a/frontend/src/components/user/create-course/SuccessStep.tsx +++ b/frontend/src/components/user/create-course/SuccessStep.tsx @@ -6,7 +6,7 @@ import { Progress } from '@/components/ui/progress'; import { CourseDraftState } from '@/types/create-course'; import { AgenticCourseResponse, AgenticCreateCourseResponse } from '@/lib/api/agentic'; import { courseApi } from '@/lib/api/course'; -import { CheckCircle, Clock, Zap, FileText, Hash } from 'lucide-react'; +import { CheckCircle, Clock, Zap, FileText, Hash, BookOpen, ChevronDown, ChevronUp } from 'lucide-react'; interface SuccessStepProps { draft: CourseDraftState; @@ -17,70 +17,75 @@ interface SuccessStepProps { type GenerationStatus = { course_id: string; + user_id?: string; + overall_status?: 'processing' | 'done' | 'error'; + error_message?: string; + title?: string; description?: string; - progress?: string; - title_ready?: boolean; - lessons_ready?: boolean; - final_ready?: boolean; - vectorized?: boolean; - lessons_count?: number; - lessons_planned?: number; - roadmap_count?: number; - final_count?: number; + vectorization_status?: string; + vectorization_chunks?: number; + + planning_status?: string; + planning_course_title?: string; + planning_roadmap_count?: number; + + lessons_status?: string; + lessons_completed?: number; + lessons_total?: number; + lessons_list?: Array<{ id: string; title: string }>; + + tests_status?: string; + tests_completed?: number; + tests_total?: number; + + start_timestamp?: number; updated_at?: string; + last_updated?: string; + end_timestamp?: number; }; function getGenerationStatus(status: GenerationStatus | null) { - // Always show 4 steps with default values, update when API returns const steps = [ { - key: 'vectorized', + key: 'vectorization', label: 'Phân tích tài liệu', - done: status?.vectorized || false, - description: 'Vectorize và index tài liệu vào OpenSearch', - count: null, + done: status?.vectorization_status === 'done', + description: `Vectorize và index tài liệu vào OpenSearch${status?.vectorization_chunks ? ` (${status.vectorization_chunks} chunks)` : ''}`, + count: status?.vectorization_chunks, icon: FileText }, { - key: 'title_ready', - label: 'Tạo kế hoạch', - done: status?.title_ready || false, - description: 'Sinh tiêu đề, mô tả và roadmap học tập', - count: status?.roadmap_count, + key: 'planning', + label: 'Tạo kế hoạch khóa học', + done: status?.planning_status === 'done', + description: `Sinh tiêu đề, mô tả và roadmap học tập${status?.planning_roadmap_count ? ` (${status.planning_roadmap_count} modules)` : ''}`, + count: status?.planning_roadmap_count, icon: Zap }, { - key: 'lesson_planned', - label: 'Lập kế hoạch bài học', - done: (status?.title_ready && status?.lessons_planned) || false, - description: `Xác định số lượng bài học cần tạo${status?.lessons_planned ? ` (${status.lessons_planned} bài)` : ''}`, - count: status?.lessons_planned, - icon: Clock - }, - { - key: 'lessons_ready', + key: 'lessons', label: 'Tạo nội dung bài học', - done: status?.lessons_ready || false, - description: `Sinh nội dung chi tiết cho từng bài học${status?.lessons_count ? ` (${status.lessons_count}/${status.lessons_planned || 0} bài)` : ''}`, - count: status?.lessons_count, + done: status?.lessons_status === 'done', + description: `Sinh nội dung chi tiết cho từng bài học${status?.lessons_completed && status?.lessons_total ? ` (${status.lessons_completed}/${status.lessons_total} bài)` : ''}`, + count: status?.lessons_completed && status?.lessons_total ? `${status.lessons_completed}/${status.lessons_total}` : status?.lessons_total, icon: CheckCircle }, + { + key: 'tests', + label: 'Tạo câu hỏi kiểm tra', + done: status?.tests_status === 'done', + description: `Sinh câu hỏi trắc nghiệm cho từng bài học${status?.tests_completed && status?.tests_total ? ` (${status.tests_completed}/${status.tests_total} bài)` : ''}`, + count: status?.tests_completed && status?.tests_total ? `${status.tests_completed}/${status.tests_total}` : status?.tests_total, + icon: Clock + }, ]; const completedSteps = steps.filter(s => s.done).length; - const isComplete = completedSteps === steps.length; + const isComplete = status?.overall_status === 'done'; const currentStep = steps.find(s => !s.done); const progressPercent = (completedSteps / steps.length) * 100; - - let overallStatus: 'done' | 'processing' | 'error' = 'processing'; - if (status) { - if (isComplete || status.final_ready === true) { - overallStatus = 'done'; - } else if (status.final_ready === false && status.progress?.includes('error')) { - overallStatus = 'error'; - } - } + const overallStatus = status?.overall_status || 'processing'; return { steps, completedSteps, isComplete, currentStep, progressPercent, overallStatus }; } @@ -101,6 +106,7 @@ const isCourseResponse = (value: AgenticCreateCourseResponse | null): value is A export function SuccessStep({ draft, result, onRestart, onGoToCourses }: SuccessStepProps) { const [generationStatus, setGenerationStatus] = useState(null); const [isPolling, setIsPolling] = useState(false); + const [isLessonsExpanded, setIsLessonsExpanded] = useState(false); const lessons = useMemo(() => (isCourseResponse(result) ? result.course_lessons : []), [result]); const hasResult = isCourseResponse(result); @@ -218,9 +224,9 @@ export function SuccessStep({ draft, result, onRestart, onGoToCourses }: Success
{/* Course Info - only show if loaded */} - {generationStatus?.title && ( + {(generationStatus?.planning_course_title || generationStatus?.title) && (
-

{generationStatus.title}

+

{generationStatus.planning_course_title || generationStatus.title}

{generationStatus.description && (

{generationStatus.description}

)} @@ -235,6 +241,19 @@ export function SuccessStep({ draft, result, onRestart, onGoToCourses }: Success
)} + {/* Error state */} + {status.overallStatus === 'error' && generationStatus?.error_message && ( +
+
+ +

Lỗi xảy ra

+
+

Quá trình tạo khóa học gặp sự cố

+

“{generationStatus.error_message}”

+

Vui lòng thử lại hoặc liên hệ hỗ trợ

+
+ )} + {/* Progress Bar */}
@@ -251,6 +270,9 @@ export function SuccessStep({ draft, result, onRestart, onGoToCourses }: Success
{status.steps.map((step) => { const Icon = step.icon; + const isLessonsStep = step.key === 'lessons'; + const hasLessons = isLessonsStep && generationStatus?.lessons_list && generationStatus.lessons_list.length > 0; + return (

{step.description}

+ + {/* Lessons List Expandable Section */} + {hasLessons && ( +
+ + + {isLessonsExpanded && ( +
+ {generationStatus.lessons_list?.map((lesson, idx) => ( +
+
+ {idx + 1} +
+
+

+ {lesson.title} +

+

+ {lesson.id} +

+
+
+ ))} +
+ )} +
+ )}
{step.done ? ( @@ -316,24 +381,24 @@ export function SuccessStep({ draft, result, onRestart, onGoToCourses }: Success {courseId}
- {generationStatus.roadmap_count && ( + {generationStatus.planning_roadmap_count && (
Roadmap: - {generationStatus.roadmap_count} bước + {generationStatus.planning_roadmap_count} modules
)} - {generationStatus.lessons_planned && ( + {generationStatus.lessons_total && (
Số bài học: - {generationStatus.lessons_count || 0}/{generationStatus.lessons_planned} bài + {generationStatus.lessons_completed || 0}/{generationStatus.lessons_total} bài
)} - {generationStatus.final_count && ( + {generationStatus.tests_total && (
- Tổng nội dung: - {generationStatus.final_count} items + Câu hỏi kiểm tra: + {generationStatus.tests_total} bài
)}
diff --git a/services/course-service/src/services/status_service.py b/services/course-service/src/services/status_service.py index e0d90afb..4ad395a6 100644 --- a/services/course-service/src/services/status_service.py +++ b/services/course-service/src/services/status_service.py @@ -56,34 +56,42 @@ def fetch_generation_status(course_id: str) -> Optional[Dict[str, Any]]: if table is None: return None try: - resp = table.get_item(Key={"course_id": course_id}) + resp = table.get_item(Key={"course_id": course_id}, ConsistentRead=True) item = resp.get("Item") if not item: return None - # Extract status fields - vectorization_status = "done" if bool(item.get("vectorized", False)) else "pending" - planning_status = "done" if bool(item.get("title_ready", False)) else "pending" - lessons_status = "done" if bool(item.get("lessons_ready", False)) else "pending" - tests_status = "done" if bool(item.get("tests_ready", False)) else "pending" + # Extract status fields - read directly from new schema + vectorization_status = item.get("vectorization_status", "pending") + planning_status = item.get("planning_status", "pending") + lessons_status = item.get("lessons_status", "pending") + tests_status = item.get("tests_status", "pending") - # Calculate overall status - all_done = ( - vectorization_status == "done" and - planning_status == "done" and - lessons_status == "done" and - tests_status == "done" - ) - overall_status = "done" if all_done else "processing" - if item.get("error_message"): - overall_status = "error" + # If tests are done, vectorization must be done + if tests_status == "done": + vectorization_status = "done" + + # Read overall_status directly from DynamoDB if available + overall_status = item.get("overall_status") + if not overall_status: + # Fallback: calculate from individual statuses + all_done = ( + vectorization_status == "done" and + planning_status == "done" and + lessons_status == "done" and + tests_status == "done" + ) + overall_status = "done" if all_done else "processing" + if item.get("error_message"): + overall_status = "error" # Build lessons list - lessons_list = [] - lessons_completed = 0 - lessons_total = item.get("lessons_count") or item.get("roadmap_count") or 0 + lessons_list = item.get("lessons_list", []) + lessons_completed = item.get("lessons_completed", 0) + lessons_total = item.get("lessons_total") or item.get("roadmap_count") or 0 - if item.get("lessons_data"): + # Fallback: calculate from lessons_data if lessons_list is empty + if not lessons_list and item.get("lessons_data"): for lesson in item.get("lessons_data", []): lessons_list.append({ "id": lesson.get("id", ""), @@ -91,8 +99,8 @@ def fetch_generation_status(course_id: str) -> Optional[Dict[str, Any]]: }) if lesson.get("completed"): lessons_completed += 1 - else: - lessons_completed = lessons_total if lessons_status == "done" else 0 + elif not lessons_completed and lessons_status == "done": + lessons_completed = lessons_total return { "overall_status": overall_status, @@ -102,8 +110,8 @@ def fetch_generation_status(course_id: str) -> Optional[Dict[str, Any]]: "vectorization_chunks": item.get("vectorization_chunks", 0), "planning_status": planning_status, - "planning_course_title": item.get("title", ""), - "planning_roadmap_count": item.get("roadmap_count", 0), + "planning_course_title": item.get("planning_course_title") or item.get("title", ""), + "planning_roadmap_count": item.get("planning_roadmap_count") or item.get("roadmap_count", 0), "lessons_status": lessons_status, "lessons_completed": lessons_completed, @@ -111,8 +119,8 @@ def fetch_generation_status(course_id: str) -> Optional[Dict[str, Any]]: "lessons_list": lessons_list, "tests_status": tests_status, - "tests_completed": lessons_total if tests_status == "done" else 0, - "tests_total": lessons_total, + "tests_completed": item.get("tests_completed") or (lessons_total if tests_status == "done" else 0), + "tests_total": item.get("tests_total") or lessons_total, "start_timestamp": item.get("start_timestamp"), "updated_at": item.get("updated_at"), @@ -150,29 +158,37 @@ def fetch_user_generations(user_id: str) -> Optional[List[Dict[str, Any]]]: # Normalize a subset of fields for frontend normalized: List[Dict[str, Any]] = [] for it in items: - # Extract status fields - vectorization_status = "done" if bool(it.get("vectorized", False)) else "pending" - planning_status = "done" if bool(it.get("title_ready", False)) else "pending" - lessons_status = "done" if bool(it.get("lessons_ready", False)) else "pending" - tests_status = "done" if bool(it.get("tests_ready", False)) else "pending" + # Extract status fields - read directly from new schema + vectorization_status = it.get("vectorization_status", "pending") + planning_status = it.get("planning_status", "pending") + lessons_status = it.get("lessons_status", "pending") + tests_status = it.get("tests_status", "pending") - # Calculate overall status - all_done = ( - vectorization_status == "done" and - planning_status == "done" and - lessons_status == "done" and - tests_status == "done" - ) - overall_status = "done" if all_done else "processing" - if it.get("error_message"): - overall_status = "error" + # If tests are done, vectorization must be done + if tests_status == "done": + vectorization_status = "done" + + # Read overall_status directly from DynamoDB if available + overall_status = it.get("overall_status") + if not overall_status: + # Fallback: calculate from individual statuses + all_done = ( + vectorization_status == "done" and + planning_status == "done" and + lessons_status == "done" and + tests_status == "done" + ) + overall_status = "done" if all_done else "processing" + if it.get("error_message"): + overall_status = "error" # Build lessons list - lessons_list = [] - lessons_completed = 0 - lessons_total = it.get("lessons_count") or it.get("roadmap_count") or 0 + lessons_list = it.get("lessons_list", []) + lessons_completed = it.get("lessons_completed", 0) + lessons_total = it.get("lessons_total") or it.get("roadmap_count") or 0 - if it.get("lessons_data"): + # Fallback: calculate from lessons_data if lessons_list is empty + if not lessons_list and it.get("lessons_data"): for lesson in it.get("lessons_data", []): lessons_list.append({ "id": lesson.get("id", ""), @@ -180,8 +196,8 @@ def fetch_user_generations(user_id: str) -> Optional[List[Dict[str, Any]]]: }) if lesson.get("completed"): lessons_completed += 1 - else: - lessons_completed = lessons_total if lessons_status == "done" else 0 + elif not lessons_completed and lessons_status == "done": + lessons_completed = lessons_total normalized.append({ "course_id": it.get("course_id"), @@ -193,8 +209,8 @@ def fetch_user_generations(user_id: str) -> Optional[List[Dict[str, Any]]]: "vectorization_chunks": it.get("vectorization_chunks", 0), "planning_status": planning_status, - "planning_course_title": it.get("title", ""), - "planning_roadmap_count": it.get("roadmap_count", 0), + "planning_course_title": it.get("planning_course_title") or it.get("title", ""), + "planning_roadmap_count": it.get("planning_roadmap_count") or it.get("roadmap_count", 0), "lessons_status": lessons_status, "lessons_completed": lessons_completed, @@ -202,8 +218,8 @@ def fetch_user_generations(user_id: str) -> Optional[List[Dict[str, Any]]]: "lessons_list": lessons_list, "tests_status": tests_status, - "tests_completed": lessons_total if tests_status == "done" else 0, - "tests_total": lessons_total, + "tests_completed": it.get("tests_completed") or (lessons_total if tests_status == "done" else 0), + "tests_total": it.get("tests_total") or lessons_total, "start_timestamp": it.get("start_timestamp"), "updated_at": it.get("updated_at"), From 446b94b6f21bb5de457007ae8a76998c556d4a69 Mon Sep 17 00:00:00 2001 From: ngochoa203 Date: Fri, 9 Jan 2026 19:13:01 +0700 Subject: [PATCH 9/9] fix --- .../user/create-course/SuccessStep.tsx | 56 +++++++------------ .../user/generation/GenerationCard.tsx | 2 +- .../user/generation/GenerationDetailModal.tsx | 30 ++++------ .../src/controllers/course_controller.py | 2 +- .../src/services/status_service.py | 12 +--- .../src/services/status_service.py | 8 +++ 6 files changed, 42 insertions(+), 68 deletions(-) diff --git a/frontend/src/components/user/create-course/SuccessStep.tsx b/frontend/src/components/user/create-course/SuccessStep.tsx index be17377c..1c13af6c 100644 --- a/frontend/src/components/user/create-course/SuccessStep.tsx +++ b/frontend/src/components/user/create-course/SuccessStep.tsx @@ -6,7 +6,7 @@ import { Progress } from '@/components/ui/progress'; import { CourseDraftState } from '@/types/create-course'; import { AgenticCourseResponse, AgenticCreateCourseResponse } from '@/lib/api/agentic'; import { courseApi } from '@/lib/api/course'; -import { CheckCircle, Clock, Zap, FileText, Hash, BookOpen, ChevronDown, ChevronUp } from 'lucide-react'; +import { CheckCircle, Clock, Zap, FileText, BookOpen, ChevronDown, ChevronUp } from 'lucide-react'; interface SuccessStepProps { draft: CourseDraftState; @@ -51,8 +51,8 @@ function getGenerationStatus(status: GenerationStatus | null) { key: 'vectorization', label: 'Phân tích tài liệu', done: status?.vectorization_status === 'done', - description: `Vectorize và index tài liệu vào OpenSearch${status?.vectorization_chunks ? ` (${status.vectorization_chunks} chunks)` : ''}`, - count: status?.vectorization_chunks, + description: 'Vectorize và index tài liệu vào OpenSearch', + count: null, icon: FileText }, { @@ -339,16 +339,13 @@ export function SuccessStep({ draft, result, onRestart, onGoToCourses }: Success key={lesson.id} className="flex items-start gap-2 p-2 bg-white rounded border border-emerald-100 hover:border-emerald-200 transition-colors" > -
+
{idx + 1}

{lesson.title}

-

- {lesson.id} -

))} @@ -372,37 +369,22 @@ export function SuccessStep({ draft, result, onRestart, onGoToCourses }: Success
{/* Additional Info */} - {generationStatus && ( -
-
- - Course ID: - - {courseId} - -
- {generationStatus.planning_roadmap_count && ( -
- Roadmap: - {generationStatus.planning_roadmap_count} modules -
- )} - {generationStatus.lessons_total && ( -
- Số bài học: - - {generationStatus.lessons_completed || 0}/{generationStatus.lessons_total} bài - -
- )} - {generationStatus.tests_total && ( -
- Câu hỏi kiểm tra: - {generationStatus.tests_total} bài -
- )} +
+
+ Roadmap: + {generationStatus?.planning_roadmap_count || '—'}
- )} +
+ Số bài học: + + {generationStatus?.lessons_completed || 0}/{generationStatus?.lessons_total || '—'} bài + +
+
+ Câu hỏi kiểm tra: + {generationStatus?.tests_total || '—'} bài +
+
)} diff --git a/frontend/src/components/user/generation/GenerationCard.tsx b/frontend/src/components/user/generation/GenerationCard.tsx index ae8926aa..acce05d4 100644 --- a/frontend/src/components/user/generation/GenerationCard.tsx +++ b/frontend/src/components/user/generation/GenerationCard.tsx @@ -39,7 +39,7 @@ function getGenerationStatus(item: GenerationItem) { key: 'vectorization', label: 'Vectorized', done: item.vectorization_status === 'done', - count: item.vectorization_chunks + count: null }, { key: 'planning', diff --git a/frontend/src/components/user/generation/GenerationDetailModal.tsx b/frontend/src/components/user/generation/GenerationDetailModal.tsx index e70a51cc..0f68e111 100644 --- a/frontend/src/components/user/generation/GenerationDetailModal.tsx +++ b/frontend/src/components/user/generation/GenerationDetailModal.tsx @@ -8,7 +8,7 @@ import { DialogDescription, } from "@/components/ui/dialog"; import { Badge } from "@/components/ui/badge"; -import { CheckCircle, Clock, Zap, Calendar, Hash, ChevronDown, ChevronUp, BookOpen } from "lucide-react"; +import { CheckCircle, Clock, Zap, Calendar, ChevronDown, ChevronUp, BookOpen } from "lucide-react"; import { GenerationItem } from "./GenerationCard"; function getDetailedStatus(item: GenerationItem) { @@ -17,8 +17,8 @@ function getDetailedStatus(item: GenerationItem) { key: 'vectorization', label: 'Phân tích tài liệu', done: item.vectorization_status === 'done', - description: `Vectorize và index tài liệu vào OpenSearch${item.vectorization_chunks ? ` (${item.vectorization_chunks} chunks)` : ''}`, - count: item.vectorization_chunks + description: 'Vectorize và index tài liệu vào OpenSearch', + count: null }, { key: 'planning', @@ -103,13 +103,12 @@ export default function GenerationDetailModal({
-
- - Course ID: - - {item.course_id} - -
+ {item.planning_roadmap_count && ( +
+ Roadmap: + {item.planning_roadmap_count} modules +
+ )} {item.lessons_total && (
Số bài học: @@ -118,12 +117,6 @@ export default function GenerationDetailModal({
)} - {item.planning_roadmap_count && ( -
- Roadmap: - {item.planning_roadmap_count} modules -
- )} {item.tests_total !== undefined && (
Câu hỏi kiểm tra: @@ -289,16 +282,13 @@ export default function GenerationDetailModal({ key={lesson.id} className="flex items-start gap-2 p-2 bg-white rounded border border-green-100 hover:border-green-200 transition-colors" > -
+
{idx + 1}

{lesson.title}

-

- {lesson.id} -

))} diff --git a/services/course-service/src/controllers/course_controller.py b/services/course-service/src/controllers/course_controller.py index 9fdcc87e..7490305b 100644 --- a/services/course-service/src/controllers/course_controller.py +++ b/services/course-service/src/controllers/course_controller.py @@ -1466,7 +1466,7 @@ def submit_assessment_controller(request: Request, course_id: str, lesson_id: st "require_exp": None, "exp_needed_for_next": None, "rank": None, - "award_failed": True, # Signal to FE that exp wasn't saved + "award_failed": True, } elif passed and not is_newly_completed: logger.info(f"⚠️ Lesson {lesson_id} retaken for practice - no exp awarded (already completed before)") diff --git a/services/course-service/src/services/status_service.py b/services/course-service/src/services/status_service.py index 4ad395a6..7520db33 100644 --- a/services/course-service/src/services/status_service.py +++ b/services/course-service/src/services/status_service.py @@ -67,8 +67,8 @@ def fetch_generation_status(course_id: str) -> Optional[Dict[str, Any]]: lessons_status = item.get("lessons_status", "pending") tests_status = item.get("tests_status", "pending") - # If tests are done, vectorization must be done - if tests_status == "done": + # If planning or lessons started, vectorization must be done + if planning_status == "pending" or lessons_status != "pending": vectorization_status = "done" # Read overall_status directly from DynamoDB if available @@ -150,28 +150,22 @@ def fetch_user_generations(user_id: str) -> Optional[List[Dict[str, Any]]]: scan_kwargs = {"FilterExpression": Attr("user_id").eq(str(user_id))} resp = table.scan(**scan_kwargs) items.extend(resp.get("Items", []) or []) - # Handle pagination while "LastEvaluatedKey" in resp: resp = table.scan(ExclusiveStartKey=resp["LastEvaluatedKey"], **scan_kwargs) items.extend(resp.get("Items", []) or []) - # Normalize a subset of fields for frontend normalized: List[Dict[str, Any]] = [] for it in items: - # Extract status fields - read directly from new schema vectorization_status = it.get("vectorization_status", "pending") planning_status = it.get("planning_status", "pending") lessons_status = it.get("lessons_status", "pending") tests_status = it.get("tests_status", "pending") - # If tests are done, vectorization must be done - if tests_status == "done": + if planning_status == "pending" or lessons_status != "pending": vectorization_status = "done" - # Read overall_status directly from DynamoDB if available overall_status = it.get("overall_status") if not overall_status: - # Fallback: calculate from individual statuses all_done = ( vectorization_status == "done" and planning_status == "done" and diff --git a/services/quiz-service/src/services/status_service.py b/services/quiz-service/src/services/status_service.py index 03f7d2f1..284cf4f0 100644 --- a/services/quiz-service/src/services/status_service.py +++ b/services/quiz-service/src/services/status_service.py @@ -80,6 +80,10 @@ def fetch_generation_status(quiz_id: str) -> Optional[Dict[str, Any]]: plan_ready = bool(item.get("plan_ready", False) or item.get("title_ready", False)) questions_ready = bool(item.get("questions_ready", False) or item.get("cards_ready", False)) + # If questions started, vectorization must be done + if questions_ready: + vectorized = True + # Status from agentic-service tracking status = item.get("status", "unknown") progress_percentage = int(item.get("progress_percentage", 0)) @@ -148,6 +152,10 @@ def fetch_user_quiz_generations(user_id: str) -> Optional[List[Dict[str, Any]]]: plan_ready = bool(it.get("plan_ready", False) or it.get("title_ready", False)) questions_ready = bool(it.get("questions_ready", False) or it.get("cards_ready", False)) + # If questions started, vectorization must be done + if questions_ready: + vectorized = True + normalized.append({ "quiz_id": it.get("quiz_id"), "user_id": it.get("user_id"),