import { useMutation, useQuery } from '@tanstack/react-query'; import { CheckIcon, ChevronLeft, ChevronRight, Loader2Icon, LockIcon, XIcon, } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; import { readAICourseLessonStream } from '../../helper/read-stream'; import { cn } from '../../lib/classname'; import { isLoggedIn, removeAuthToken } from '../../lib/jwt'; import { markdownToHtml, markdownToHtmlWithHighlighting, } from '../../lib/markdown'; import { httpPatch } from '../../lib/query-http'; import { slugify } from '../../lib/slugger'; import { getAiCourseLimitOptions, getAiCourseProgressOptions, type AICourseProgressDocument, } from '../../queries/ai-course'; import { queryClient } from '../../stores/query-client'; import { AICourseFollowUp } from './AICourseFollowUp'; import './AICourseFollowUp.css'; type AICourseModuleViewProps = { courseSlug: string; activeModuleIndex: number; totalModules: number; currentModuleTitle: string; activeLessonIndex: number; totalLessons: number; currentLessonTitle: string; onGoToPrevLesson: () => void; onGoToNextLesson: () => void; onUpgrade: () => void; }; export function AICourseModuleView(props: AICourseModuleViewProps) { const { courseSlug, activeModuleIndex, totalModules, currentModuleTitle, activeLessonIndex, totalLessons, currentLessonTitle, onGoToPrevLesson, onGoToNextLesson, onUpgrade, } = props; const [isLoading, setIsLoading] = useState(true); const [isGenerating, setIsGenerating] = useState(false); const [error, setError] = useState(''); const [lessonHtml, setLessonHtml] = useState(''); const { data: aiCourseProgress } = useQuery( getAiCourseProgressOptions({ aiCourseSlug: courseSlug || '' }), queryClient, ); const lessonId = `${slugify(currentModuleTitle)}__${slugify(currentLessonTitle)}`; const isLessonDone = aiCourseProgress?.done.includes(lessonId); const abortController = useMemo( () => new AbortController(), [activeModuleIndex, activeLessonIndex], ); const generateAiCourseContent = async () => { setIsLoading(true); setError(''); setLessonHtml(''); if (!isLoggedIn()) { setIsLoading(false); setError('Please login to generate course content'); return; } if (!currentModuleTitle || !currentLessonTitle) { setIsLoading(false); setError('Invalid module title or lesson title'); return; } const response = await fetch( `${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-course-lesson/${courseSlug}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, signal: abortController.signal, credentials: 'include', body: JSON.stringify({ moduleTitle: currentModuleTitle, lessonTitle: currentLessonTitle, modulePosition: activeModuleIndex, lessonPosition: activeLessonIndex, totalLessonsInModule: totalLessons, }), }, ); if (!response.ok) { const data = await response.json(); setError(data?.message || 'Something went wrong'); setIsLoading(false); // Logout user if token is invalid if (data.status === 401) { removeAuthToken(); window.location.reload(); } } const reader = response.body?.getReader(); if (!reader) { setIsLoading(false); setError('Something went wrong'); return; } setIsLoading(false); setIsGenerating(true); await readAICourseLessonStream(reader, { onStream: async (result) => { if (abortController.signal.aborted) { return; } setLessonHtml(markdownToHtml(result, false)); }, onStreamEnd: async (result) => { if (abortController.signal.aborted) { return; } setLessonHtml(await markdownToHtmlWithHighlighting(result)); queryClient.invalidateQueries(getAiCourseLimitOptions()); setIsGenerating(false); }, }); }; const { mutate: toggleDone, isPending: isTogglingDone } = useMutation( { mutationFn: () => { return httpPatch( `/v1-toggle-done-ai-lesson/${courseSlug}`, { lessonId, }, ); }, onSuccess: (data) => { queryClient.setQueryData( ['ai-course-progress', { aiCourseSlug: courseSlug }], data, ); }, }, queryClient, ); useEffect(() => { generateAiCourseContent(); }, [currentModuleTitle, currentLessonTitle]); useEffect(() => { return () => { abortController.abort(); }; }, [abortController]); const cantGoForward = (activeModuleIndex === totalModules - 1 && activeLessonIndex === totalLessons - 1) || isGenerating || isLoading; const cantGoBack = (activeModuleIndex === 0 && activeLessonIndex === 0) || isGenerating; return (
{(isGenerating || isLoading) && (
)}
Lesson {activeLessonIndex + 1} of {totalLessons}
{!isGenerating && !isLoading && ( <> )}

{currentLessonTitle?.replace(/^Lesson\s*?\d+[\.:]\s*/, '')}

{!error && isLoggedIn() && (
)} {error && isLoggedIn() && (
{error.includes('reached the limit') ? (

Limit reached

You have reached the AI usage limit for today. Please upgrade your account to continue.

) : (

{error}

)}
)} {!isLoggedIn() && (

Please login to generate course content

)}
{!isGenerating && !isLoading && ( )}
); }