Improve AI courses

pull/8331/head
Kamran Ahmed 1 month ago
parent fbd149f955
commit 3ba9abe7e3
  1. 26
      src/components/GenerateCourse/AICourseContent.tsx
  2. 32
      src/components/GenerateCourse/AICourseLesson.tsx
  3. 24
      src/components/GenerateCourse/AICourseSidebarModuleList.tsx
  4. 1
      src/components/GenerateCourse/GenerateAICourse.tsx
  5. 22
      src/components/GenerateCourse/GetAICourse.tsx
  6. 1
      src/helper/generate-ai-course.ts
  7. 1
      src/lib/ai.ts
  8. 21
      src/queries/ai-course.ts

@ -1,28 +1,25 @@
import { useQuery } from '@tanstack/react-query';
import {
BookOpenCheck,
ChevronLeft,
CircleAlert,
Loader2,
Menu,
X,
CircleAlert,
Play,
X,
} from 'lucide-react';
import { useState } from 'react';
import { type AiCourse } from '../../lib/ai';
import { cn } from '../../lib/classname';
import { slugify } from '../../lib/slugger';
import { getAiCourseProgressOptions } from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
import { useIsPaidUser } from '../../queries/billing';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { CheckIcon } from '../ReactIcons/CheckIcon';
import { ErrorIcon } from '../ReactIcons/ErrorIcon';
import { AICourseLesson } from './AICourseLesson';
import { AICourseLimit } from './AICourseLimit';
import { AICourseSidebarModuleList } from './AICourseSidebarModuleList';
import { AICourseLesson } from './AICourseLesson';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { AILimitsPopup } from './AILimitsPopup';
import { RegenerateOutline } from './RegenerateOutline';
import { useIsPaidUser } from '../../queries/billing';
type AICourseContentProps = {
courseSlug?: string;
@ -45,10 +42,7 @@ export function AICourseContent(props: AICourseContentProps) {
const { isPaidUser } = useIsPaidUser();
const { data: aiCourseProgress } = useQuery(
getAiCourseProgressOptions({ aiCourseSlug: courseSlug || '' }),
queryClient,
);
const aiCourseProgress = course.done || [];
const [expandedModules, setExpandedModules] = useState<
Record<number, boolean>
@ -121,7 +115,7 @@ export function AICourseContent(props: AICourseContentProps) {
0,
);
const totalDoneLessons = aiCourseProgress?.done?.length || 0;
const totalDoneLessons = (course?.done || []).length;
const finishedPercentage = Math.round(
(totalDoneLessons / totalCourseLessons) * 100,
);
@ -389,6 +383,7 @@ export function AICourseContent(props: AICourseContentProps) {
{viewMode === 'module' && (
<AICourseLesson
courseSlug={courseSlug!}
progress={aiCourseProgress}
activeModuleIndex={activeModuleIndex}
totalModules={totalModules}
currentModuleTitle={currentModule?.title || ''}
@ -438,9 +433,8 @@ export function AICourseContent(props: AICourseContentProps) {
</h2>
<div className="divide-y divide-gray-100">
{courseModule.lessons.map((lesson, lessonIdx) => {
const key = `${slugify(courseModule.title)}__${slugify(lesson)}`;
const isCompleted =
aiCourseProgress?.done.includes(key);
const key = `${slugify(String(moduleIdx))}-${slugify(String(lessonIdx))}`;
const isCompleted = aiCourseProgress.includes(key);
return (
<div

@ -1,4 +1,4 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import {
CheckIcon,
ChevronLeft,
@ -8,6 +8,7 @@ import {
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';
@ -19,16 +20,16 @@ import { httpPatch } from '../../lib/query-http';
import { slugify } from '../../lib/slugger';
import {
getAiCourseLimitOptions,
getAiCourseProgressOptions,
type AICourseProgressDocument,
getAiCourseOptions
} from '../../queries/ai-course';
import { useIsPaidUser } from '../../queries/billing';
import { queryClient } from '../../stores/query-client';
import { AICourseFollowUp } from './AICourseFollowUp';
import './AICourseFollowUp.css';
import { useIsPaidUser } from '../../queries/billing';
type AICourseLessonProps = {
courseSlug: string;
progress: string[];
activeModuleIndex: number;
totalModules: number;
@ -46,6 +47,7 @@ type AICourseLessonProps = {
export function AICourseLesson(props: AICourseLessonProps) {
const {
courseSlug,
progress = [],
activeModuleIndex,
totalModules,
@ -65,13 +67,9 @@ export function AICourseLesson(props: AICourseLessonProps) {
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 lessonId = `${slugify(String(activeModuleIndex))}-${slugify(String(activeLessonIndex))}`;
const isLessonDone = progress?.includes(lessonId);
const { isPaidUser } = useIsPaidUser();
@ -107,11 +105,8 @@ export function AICourseLesson(props: AICourseLessonProps) {
signal: abortController.signal,
credentials: 'include',
body: JSON.stringify({
moduleTitle: currentModuleTitle,
lessonTitle: currentLessonTitle,
modulePosition: activeModuleIndex,
lessonPosition: activeLessonIndex,
totalLessonsInModule: totalLessons,
moduleIndex: activeModuleIndex,
lessonIndex: activeLessonIndex,
}),
},
);
@ -167,16 +162,17 @@ export function AICourseLesson(props: AICourseLessonProps) {
const { mutate: toggleDone, isPending: isTogglingDone } = useMutation(
{
mutationFn: () => {
return httpPatch<AICourseProgressDocument>(
return httpPatch<AICourseDocument>(
`/v1-toggle-done-ai-lesson/${courseSlug}`,
{
lessonId,
moduleIndex: activeModuleIndex,
lessonIndex: activeLessonIndex,
},
);
},
onSuccess: (data) => {
queryClient.setQueryData(
['ai-course-progress', { aiCourseSlug: courseSlug }],
getAiCourseOptions({ aiCourseSlug: courseSlug }).queryKey,
data,
);
},

@ -2,9 +2,6 @@ import { type Dispatch, type SetStateAction } from 'react';
import type { AiCourse } from '../../lib/ai';
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';
@ -44,10 +41,7 @@ export function AICourseSidebarModuleList(props: AICourseModuleListProps) {
isLoading,
} = props;
const { data: aiCourseProgress } = useQuery(
getAiCourseProgressOptions({ aiCourseSlug: courseSlug || '' }),
queryClient,
);
const aiCourseProgress = course.done || [];
const toggleModule = (index: number) => {
setExpandedModules((prev) => {
@ -71,16 +65,18 @@ export function AICourseSidebarModuleList(props: AICourseModuleListProps) {
});
};
const { done = [] } = aiCourseProgress || {};
const done = aiCourseProgress || [];
return (
<nav className="bg-gray-100">
{course.modules.map((courseModule, moduleIdx) => {
const totalLessons = courseModule.lessons.length;
const completedLessons = courseModule.lessons.filter((lesson) => {
const key = `${slugify(courseModule.title)}__${slugify(lesson)}`;
return done.includes(key);
}).length;
const completedLessons = courseModule.lessons.filter(
(lesson, lessonIdx) => {
const key = `${slugify(String(moduleIdx))}-${slugify(String(lessonIdx))}`;
return done.includes(key);
},
).length;
const percentage = Math.round((completedLessons / totalLessons) * 100);
const isActive = expandedModules[moduleIdx];
@ -139,7 +135,7 @@ export function AICourseSidebarModuleList(props: AICourseModuleListProps) {
{expandedModules[moduleIdx] && (
<div className="flex flex-col border-b border-b-gray-200 bg-gray-100">
{courseModule.lessons.map((lesson, lessonIdx) => {
const key = `${slugify(courseModule.title)}__${slugify(lesson)}`;
const key = `${slugify(String(moduleIdx))}-${slugify(String(lessonIdx))}`;
const isCompleted = done.includes(key);
return (
@ -160,7 +156,7 @@ export function AICourseSidebarModuleList(props: AICourseModuleListProps) {
setViewMode('module');
}}
className={cn(
'flex gap-2.5 w-full cursor-pointer items-center py-3 pl-3.5 pr-2 text-left text-sm leading-normal',
'flex w-full cursor-pointer items-center gap-2.5 py-3 pl-3.5 pr-2 text-left text-sm leading-normal',
activeModuleIndex === moduleIdx &&
activeLessonIndex === lessonIdx
? 'bg-gray-200 text-black'

@ -20,6 +20,7 @@ export function GenerateAICourse(props: GenerateAICourseProps) {
title: '',
modules: [],
difficulty: '',
done: [],
});
useEffect(() => {

@ -1,8 +1,5 @@
import { useQuery } from '@tanstack/react-query';
import {
getAiCourseOptions,
getAiCourseProgressOptions,
} from '../../queries/ai-course';
import { getAiCourseOptions } from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
import { useEffect, useState } from 'react';
import { AICourseContent } from './AICourseContent';
@ -24,12 +21,6 @@ export function GetAICourse(props: GetAICourseProps) {
const { data: aiCourse, error: queryError } = useQuery(
{
...getAiCourseOptions({ aiCourseSlug: courseSlug }),
select: (data) => {
return {
...data,
course: generateAiCourseStructure(data.data),
};
},
enabled: !!courseSlug && !!isLoggedIn(),
},
queryClient,
@ -75,18 +66,14 @@ export function GetAICourse(props: GetAICourseProps) {
...aiCourse,
title: course.title,
difficulty: course.difficulty,
data: rawData,
modules: course.modules,
},
);
},
onLoadingChange: (isNewLoading) => {
setIsRegenerating(isNewLoading);
if (!isNewLoading) {
queryClient.invalidateQueries({
queryKey: getAiCourseProgressOptions({
aiCourseSlug: courseSlug,
}).queryKey,
});
// TODO: Update progress
}
},
onError: setError,
@ -98,8 +85,9 @@ export function GetAICourse(props: GetAICourseProps) {
<AICourseContent
course={{
title: aiCourse?.title || '',
modules: aiCourse?.course.modules || [],
modules: aiCourse?.modules || [],
difficulty: aiCourse?.difficulty || 'Easy',
done: aiCourse?.done || [],
}}
isLoading={isLoading || isRegenerating}
courseSlug={courseSlug}

@ -39,6 +39,7 @@ export async function generateCourse(options: GenerateCourseOptions) {
title: '',
modules: [],
difficulty: '',
done: [],
},
'',
);

@ -11,6 +11,7 @@ export type AiCourse = {
title: string;
modules: Module[];
difficulty: string;
done: string[];
};
export function generateAiCourseStructure(

@ -11,24 +11,11 @@ export interface AICourseProgressDocument {
updatedAt: Date;
}
type GetAICourseProgressParams = {
aiCourseSlug: string;
type AICourseModule = {
title: string;
lessons: string[];
};
type GetAICourseProgressResponse = AICourseProgressDocument;
export function getAiCourseProgressOptions(params: GetAICourseProgressParams) {
return {
queryKey: ['ai-course-progress', params],
queryFn: () => {
return httpGet<GetAICourseProgressResponse>(
`/v1-get-ai-course-progress/${params.aiCourseSlug}`,
);
},
enabled: !!params.aiCourseSlug && isLoggedIn(),
};
}
type GetAICourseParams = {
aiCourseSlug: string;
};
@ -41,7 +28,7 @@ export interface AICourseDocument {
keyword: string;
done: string[];
difficulty: string;
data: string;
modules: AICourseModule[];
viewCount: number;
createdAt: Date;
updatedAt: Date;

Loading…
Cancel
Save