diff --git a/src/components/GenerateCourse/AICourseModuleList.tsx b/src/components/GenerateCourse/AICourseModuleList.tsx
index 3ed31bb01..6fa4f85c7 100644
--- a/src/components/GenerateCourse/AICourseModuleList.tsx
+++ b/src/components/GenerateCourse/AICourseModuleList.tsx
@@ -1,16 +1,13 @@
import { type Dispatch, type SetStateAction, useState } from 'react';
import type { AiCourse } from '../../lib/ai';
-import {
- CheckCircleIcon,
- ChevronDownIcon,
- ChevronRightIcon,
-} from 'lucide-react';
+import { Check, ChevronDownIcon, ChevronRightIcon } from 'lucide-react';
import { cn } from '../../lib/classname';
import { getAiCourseProgressOptions } from '../../queries/ai-course';
import { useQuery } from '@tanstack/react-query';
import { queryClient } from '../../stores/query-client';
import { slugify } from '../../lib/slugger';
import { CheckIcon } from '../ReactIcons/CheckIcon';
+import { CircularProgress } from './CircularProgress';
type AICourseModuleListProps = {
course: AiCourse;
@@ -43,7 +40,7 @@ export function AICourseModuleList(props: AICourseModuleListProps) {
setExpandedModules,
} = props;
- const { data: aiCourseProgress } = useQuery(
+ const { data: aiCourseProgress, isLoading } = useQuery(
getAiCourseProgressOptions({ aiCourseSlug: courseSlug || '' }),
queryClient,
);
@@ -74,85 +71,115 @@ export function AICourseModuleList(props: AICourseModuleListProps) {
return (
);
}
diff --git a/src/components/GenerateCourse/AICourseModuleView.tsx b/src/components/GenerateCourse/AICourseModuleView.tsx
index fb7730250..985d72bfe 100644
--- a/src/components/GenerateCourse/AICourseModuleView.tsx
+++ b/src/components/GenerateCourse/AICourseModuleView.tsx
@@ -1,6 +1,6 @@
import { ChevronLeft, ChevronRight, Loader2Icon, LockIcon } from 'lucide-react';
import { cn } from '../../lib/classname';
-import { useEffect, useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import { isLoggedIn, removeAuthToken } from '../../lib/jwt';
import { readAICourseLessonStream } from '../../helper/read-stream';
import { markdownToHtml } from '../../lib/markdown';
@@ -52,6 +52,11 @@ export function AICourseModuleView(props: AICourseModuleViewProps) {
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('');
@@ -76,6 +81,7 @@ export function AICourseModuleView(props: AICourseModuleViewProps) {
headers: {
'Content-Type': 'application/json',
},
+ signal: abortController.signal,
credentials: 'include',
body: JSON.stringify({
moduleTitle: currentModuleTitle,
@@ -111,9 +117,17 @@ export function AICourseModuleView(props: AICourseModuleViewProps) {
setIsGenerating(true);
await readAICourseLessonStream(reader, {
onStream: async (result) => {
+ if (abortController.signal.aborted) {
+ return;
+ }
+
setLessonHtml(markdownToHtml(result, false));
},
onStreamEnd: () => {
+ if (abortController.signal.aborted) {
+ return;
+ }
+
setIsGenerating(false);
},
});
@@ -126,6 +140,13 @@ export function AICourseModuleView(props: AICourseModuleViewProps) {
lessonId,
});
},
+ onSuccess: () => {
+ queryClient.invalidateQueries(
+ getAiCourseProgressOptions({
+ aiCourseSlug: courseSlug || '',
+ }),
+ );
+ },
},
queryClient,
);
@@ -134,6 +155,12 @@ export function AICourseModuleView(props: AICourseModuleViewProps) {
generateAiCourseContent();
}, [currentModuleTitle, currentLessonTitle]);
+ useEffect(() => {
+ return () => {
+ abortController.abort();
+ };
+ }, [abortController]);
+
return (
diff --git a/src/components/GenerateCourse/CircularProgress.tsx b/src/components/GenerateCourse/CircularProgress.tsx
new file mode 100644
index 000000000..67b6386e9
--- /dev/null
+++ b/src/components/GenerateCourse/CircularProgress.tsx
@@ -0,0 +1,57 @@
+import { cn } from '../../lib/classname';
+
+export function ChapterNumberSkeleton() {
+ return (
+
+ );
+}
+
+type CircularProgressProps = {
+ percentage: number;
+ children: React.ReactNode;
+ isVisible?: boolean;
+ isActive?: boolean;
+ isLoading?: boolean;
+};
+
+export function CircularProgress(props: CircularProgressProps) {
+ const {
+ percentage,
+ children,
+ isVisible = true,
+ isActive = false,
+ isLoading = false,
+ } = props;
+
+ const circumference = 2 * Math.PI * 13;
+ const strokeDasharray = `${circumference}`;
+ const strokeDashoffset = circumference - (percentage / 100) * circumference;
+
+ return (
+
+ {isVisible && !isLoading && (
+
+ )}
+
+ {!isLoading && children}
+ {isLoading && }
+
+ );
+}