refactor/ai-courses
Kamran Ahmed 2 months ago
parent cfbb4f32ab
commit c87a7c0ddf
  1. 1
      src/components/GenerateCourse/GenerateAICourse.tsx
  2. 16
      src/components/GenerateCourse/GetAICourse.tsx
  3. 2
      src/components/GenerateCourse/RegenerateOutline.tsx
  4. 21
      src/components/GenerateCourse/UserCoursesList.tsx
  5. 35
      src/helper/generate-ai-course.ts
  6. 12
      src/helper/number.ts
  7. 29
      src/helper/read-stream.ts
  8. 7
      src/queries/billing.ts

@ -54,6 +54,7 @@ export function GenerateAICourse(props: GenerateAICourseProps) {
await generateCourse({ await generateCourse({
term, term,
difficulty, difficulty,
slug: courseSlug,
onCourseIdChange: setCourseId, onCourseIdChange: setCourseId,
onCourseSlugChange: setCourseSlug, onCourseSlugChange: setCourseSlug,
onCourseChange: setCourse, onCourseChange: setCourse,

@ -15,6 +15,8 @@ export function GetAICourse(props: GetAICourseProps) {
const { courseSlug } = props; const { courseSlug } = props;
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isRegenerating, setIsRegenerating] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const { data: aiCourse, error: queryError } = useQuery( const { data: aiCourse, error: queryError } = useQuery(
{ {
@ -61,7 +63,17 @@ export function GetAICourse(props: GetAICourseProps) {
await generateCourse({ await generateCourse({
term: aiCourse.keyword, term: aiCourse.keyword,
difficulty: aiCourse.difficulty, difficulty: aiCourse.difficulty,
onLoadingChange: setIsLoading, slug: courseSlug,
onCourseChange: (course, rawData) => {
queryClient.setQueryData(
getAiCourseOptions({ aiCourseSlug: courseSlug }).queryKey,
{
...aiCourse,
data: rawData,
},
);
},
onLoadingChange: setIsRegenerating,
onError: setError, onError: setError,
isForce: true, isForce: true,
}); });
@ -74,7 +86,7 @@ export function GetAICourse(props: GetAICourseProps) {
modules: aiCourse?.course.modules || [], modules: aiCourse?.course.modules || [],
difficulty: aiCourse?.difficulty || 'Easy', difficulty: aiCourse?.difficulty || 'Easy',
}} }}
isLoading={isLoading} isLoading={isLoading || isRegenerating}
courseSlug={courseSlug} courseSlug={courseSlug}
error={error} error={error}
onRegenerateOutline={handleRegenerateCourse} onRegenerateOutline={handleRegenerateCourse}

@ -16,7 +16,7 @@ export function RegenerateOutline(props: RegenerateOutlineProps) {
const [showUpgradeModal, setShowUpgradeModal] = useState(false); const [showUpgradeModal, setShowUpgradeModal] = useState(false);
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const isPaidUser = useIsPaidUser(); const { isPaidUser } = useIsPaidUser();
useOutsideClick(ref, () => setIsDropdownVisible(false)); useOutsideClick(ref, () => setIsDropdownVisible(false));

@ -10,7 +10,7 @@ import { Gift, Loader2, Search, User2 } from 'lucide-react';
import { isLoggedIn } from '../../lib/jwt'; import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup'; import { showLoginPopup } from '../../lib/popup';
import { cn } from '../../lib/classname'; import { cn } from '../../lib/classname';
import { billingDetailsOptions } from '../../queries/billing'; import { useIsPaidUser } from '../../queries/billing';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal'; import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
type UserCoursesListProps = {}; type UserCoursesListProps = {};
@ -20,17 +20,13 @@ export function UserCoursesList(props: UserCoursesListProps) {
const [isInitialLoading, setIsInitialLoading] = useState(true); const [isInitialLoading, setIsInitialLoading] = useState(true);
const [showUpgradePopup, setShowUpgradePopup] = useState(false); const [showUpgradePopup, setShowUpgradePopup] = useState(false);
const { data: limits, isLoading } = useQuery( const { data: limits, isLoading: isLimitsLoading } = useQuery(
getAiCourseLimitOptions(), getAiCourseLimitOptions(),
queryClient, queryClient,
); );
const { used, limit } = limits ?? { used: 0, limit: 0 }; const { used, limit } = limits ?? { used: 0, limit: 0 };
const { isPaidUser, isLoading: isPaidUserLoading } = useIsPaidUser();
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
useQuery(billingDetailsOptions(), queryClient);
const isPaidUser = userBillingDetails?.status !== 'active';
const { data: userAiCourses, isFetching: isUserAiCoursesLoading } = useQuery( const { data: userAiCourses, isFetching: isUserAiCoursesLoading } = useQuery(
listUserAiCoursesOptions(), listUserAiCoursesOptions(),
@ -55,13 +51,6 @@ export function UserCoursesList(props: UserCoursesListProps) {
}); });
const isAuthenticated = isLoggedIn(); const isAuthenticated = isLoggedIn();
const canSearch =
!isInitialLoading &&
!isUserAiCoursesLoading &&
isAuthenticated &&
userAiCourses?.length !== 0;
const limitUsedPercentage = Math.round((used / limit) * 100); const limitUsedPercentage = Math.round((used / limit) * 100);
return ( return (
@ -72,11 +61,12 @@ export function UserCoursesList(props: UserCoursesListProps) {
<div className="mb-3 flex min-h-[35px] items-center justify-between max-sm:mb-1"> <div className="mb-3 flex min-h-[35px] items-center justify-between max-sm:mb-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h2 className="text-lg font-semibold"> <h2 className="text-lg font-semibold">
<span className='max-md:hidden'>Your </span>Courses <span className="max-md:hidden">Your </span>Courses
</h2> </h2>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{used > 0 && limit > 0 && !isPaidUserLoading && (
<div <div
className={cn( className={cn(
'flex items-center gap-2 opacity-0 transition-opacity', 'flex items-center gap-2 opacity-0 transition-opacity',
@ -103,6 +93,7 @@ export function UserCoursesList(props: UserCoursesListProps) {
</button> </button>
</p> </p>
</div> </div>
)}
<div className={cn('relative w-64 max-sm:hidden', {})}> <div className={cn('relative w-64 max-sm:hidden', {})}>
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">

@ -9,10 +9,11 @@ import { getAiCourseLimitOptions } from '../queries/ai-course';
type GenerateCourseOptions = { type GenerateCourseOptions = {
term: string; term: string;
difficulty: string; difficulty: string;
slug?: string;
isForce?: boolean; isForce?: boolean;
onCourseIdChange?: (courseId: string) => void; onCourseIdChange?: (courseId: string) => void;
onCourseSlugChange?: (courseSlug: string) => void; onCourseSlugChange?: (courseSlug: string) => void;
onCourseChange?: (course: AiCourse) => void; onCourseChange?: (course: AiCourse, rawData: string) => void;
onLoadingChange?: (isLoading: boolean) => void; onLoadingChange?: (isLoading: boolean) => void;
onError?: (error: string) => void; onError?: (error: string) => void;
}; };
@ -20,6 +21,7 @@ type GenerateCourseOptions = {
export async function generateCourse(options: GenerateCourseOptions) { export async function generateCourse(options: GenerateCourseOptions) {
const { const {
term, term,
slug,
difficulty, difficulty,
onCourseIdChange, onCourseIdChange,
onCourseSlugChange, onCourseSlugChange,
@ -30,15 +32,32 @@ export async function generateCourse(options: GenerateCourseOptions) {
} = options; } = options;
onLoadingChange?.(true); onLoadingChange?.(true);
onCourseChange?.({ onCourseChange?.(
{
title: '', title: '',
modules: [], modules: [],
difficulty: '', difficulty: '',
}); },
'',
);
onError?.(''); onError?.('');
try { try {
const response = await fetch( let response = null;
if (slug && isForce) {
response = await fetch(
`${import.meta.env.PUBLIC_API_URL}/v1-regenerate-ai-course/${slug}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
},
);
} else {
response = await fetch(
`${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-course`, `${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-course`,
{ {
method: 'POST', method: 'POST',
@ -53,6 +72,7 @@ export async function generateCourse(options: GenerateCourseOptions) {
credentials: 'include', credentials: 'include',
}, },
); );
}
if (!response.ok) { if (!response.ok) {
const data = await response.json(); const data = await response.json();
@ -108,10 +128,13 @@ export async function generateCourse(options: GenerateCourseOptions) {
try { try {
const aiCourse = generateAiCourseStructure(result); const aiCourse = generateAiCourseStructure(result);
onCourseChange?.({ onCourseChange?.(
{
...aiCourse, ...aiCourse,
difficulty: difficulty || '', difficulty: difficulty || '',
}); },
result,
);
} catch (e) { } catch (e) {
console.error('Error parsing streamed course content:', e); console.error('Error parsing streamed course content:', e);
} }

@ -1,12 +0,0 @@
export function getPercentage(portion: number, total: number): number {
if (portion <= 0 || total <= 0) {
return 0;
}
if (portion >= total) {
return 100;
}
const percentage = (portion / total) * 100;
return Math.round(percentage);
}

@ -1,29 +0,0 @@
const NEW_LINE = '\n'.charCodeAt(0);
export async function readAICourseLessonStream(
reader: ReadableStreamDefaultReader<Uint8Array>,
{
onStream,
onStreamEnd,
}: {
onStream?: (lesson: string) => void;
onStreamEnd?: (lesson: string) => void;
},
) {
const decoder = new TextDecoder('utf-8');
let result = '';
while (true) {
const { value, done } = await reader.read();
if (done) {
break;
}
result += decoder.decode(value);
onStream?.(result);
}
onStream?.(result);
onStreamEnd?.(result);
reader.releaseLock();
}

@ -55,7 +55,7 @@ export function billingDetailsOptions() {
} }
export function useIsPaidUser() { export function useIsPaidUser() {
const { data } = useQuery( const { data, isLoading } = useQuery(
{ {
queryKey: ['billing-details'], queryKey: ['billing-details'],
queryFn: async () => { queryFn: async () => {
@ -67,7 +67,10 @@ export function useIsPaidUser() {
queryClient, queryClient,
); );
return data ?? false; return {
isPaidUser: data ?? false,
isLoading,
};
} }
type CoursePriceParams = { type CoursePriceParams = {

Loading…
Cancel
Save