import { useMutation } from '@tanstack/react-query'; import { CheckIcon, ChevronLeft, ChevronRight, Loader2Icon, LockIcon, XIcon, } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; import type { AICourseDocument } from '../../api/ai-roadmap'; import { readStream } from '../../lib/ai'; 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, getAiCourseOptions, } from '../../queries/ai-course'; import { useIsPaidUser } from '../../queries/billing'; import { queryClient } from '../../stores/query-client'; import './AICourseLessonChat.css'; import { RegenerateLesson } from './RegenerateLesson'; import { TestMyKnowledgeAction } from './TestMyKnowledgeAction'; import { AICourseLessonChat } from './AICourseLessonChat'; type AICourseLessonProps = { courseSlug: string; progress: string[]; activeModuleIndex: number; totalModules: number; currentModuleTitle: string; activeLessonIndex: number; totalLessons: number; currentLessonTitle: string; onGoToPrevLesson: () => void; onGoToNextLesson: () => void; onUpgrade: () => void; }; export function AICourseLesson(props: AICourseLessonProps) { const { courseSlug, progress = [], 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 lessonId = `${slugify(String(activeModuleIndex))}-${slugify(String(activeLessonIndex))}`; const isLessonDone = progress?.includes(lessonId); const { isPaidUser } = useIsPaidUser(); const abortController = useMemo( () => new AbortController(), [activeModuleIndex, activeLessonIndex], ); const generateAiCourseContent = async ( isForce?: boolean, customPrompt?: string, ) => { 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({ moduleIndex: activeModuleIndex, lessonIndex: activeLessonIndex, isForce, customPrompt, }), }, ); 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(); } return; } if (!response.body) { setIsLoading(false); setError('No response body received'); return; } try { const reader = response.body.getReader(); setIsLoading(false); setIsGenerating(true); await readStream(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); }, }); } catch (e) { setError(e instanceof Error ? e.message : 'Something went wrong'); setIsLoading(false); } }; const { mutate: toggleDone, isPending: isTogglingDone } = useMutation( { mutationFn: () => { return httpPatch( `/v1-toggle-done-ai-lesson/${courseSlug}`, { moduleIndex: activeModuleIndex, lessonIndex: activeLessonIndex, }, ); }, onSuccess: (data) => { queryClient.setQueryData( getAiCourseOptions({ aiCourseSlug: courseSlug }).queryKey, 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 || isLoading; return (
{(isGenerating || isLoading) && (
)}
Lesson {activeLessonIndex + 1} of {totalLessons}
{!isGenerating && !isLoading && (
{ generateAiCourseContent(true, prompt); }} />
)}

{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. {!isPaidUser && ( <>Please upgrade your account to continue. )} {isPaidUser && ( <> Please wait until tomorrow to continue. )}

{!isPaidUser && ( )}
) : (

{error}

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

Please login to generate course content

)} {!isLoading && !isGenerating && !error && ( )}
AI can make mistakes, check important info.
); }