Improve AI courses
This commit is contained in:
1 parent
fbd149f955
commit
3ba9abe7e3
8 files changed
+46
-82
No files matched your search
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user