Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion frontend/src/app/user/my-courses/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/app/user/my-quizzes/[quizId]/play/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(() => {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/user/ranking/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/app/user/tracking/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.updated_at) return 1;
if (!b.updated_at) return -1;
return new Date(b.updated_at).getTime() - new Date(a.updated_at).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) {
Expand Down
36 changes: 34 additions & 2 deletions frontend/src/components/user/chatbot/LessonChatbot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default function LessonChatbot({ lessonId, courseId }: ChatbotProps) {
const [inputValue, setInputValue] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [pendingChatId, setPendingChatId] = useState<string | null>(null);
const [pollStartTime, setPollStartTime] = useState<number | null>(null);

const messagesEndRef = useRef<HTMLDivElement>(null);
const lastActivityRef = useRef<number>(Date.now());
Expand Down Expand Up @@ -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) {

Copilot AI Jan 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pollStartTime is initialized to null but never set to Date.now() when polling actually starts. This means the timeout check on line 82 will never trigger because pollStartTime remains null. You should set setPollStartTime(Date.now()) when polling begins, such as right after calling setPendingChatId in the handleSendMessage function (around line 182).

Copilot uses AI. Check for mistakes.
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;

Expand All @@ -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 => [
Expand All @@ -103,11 +124,21 @@ 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: unknown) {
// If 404, treat as still processing (Lambda hasn't created record yet)
if (error && typeof error === 'object' && 'response' in error) {
const err = error as { response?: { status?: number } };
if (err.response?.status === 404) {
return; // Continue polling
}
}
} catch (error) {
console.error('Error polling answer:', error);
setPendingChatId(null);
setIsLoading(false);
setPollStartTime(null);
}
};

Expand All @@ -116,10 +147,11 @@ export default function LessonChatbot({ lessonId, courseId }: ChatbotProps) {

return () => {
if (pollingIntervalRef.current) {
setPollStartTime(Date.now()); // Start timeout timer

Copilot AI Jan 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pollStartTime is being set inside the cleanup function, which runs when the effect is torn down (when pendingChatId changes or component unmounts). This is backwards - the timeout timer should be started when polling begins, not when it ends. The setPollStartTime call should be moved to line 143 (after pollAnswer is called initially) to properly track when polling started.

Copilot uses AI. Check for mistakes.

Copilot AI Jan 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The indentation of this line is incorrect. It should be aligned with the clearInterval call on line 148, not nested inside the if block condition.

Suggested change
setPollStartTime(Date.now()); // Start timeout timer
setPollStartTime(Date.now()); // Start timeout timer

Copilot uses AI. Check for mistakes.
clearInterval(pollingIntervalRef.current);
}
};
}, [pendingChatId]);
}, [pendingChatId, pollStartTime]);

useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
Expand Down
Loading