refactor/ai-courses
Kamran Ahmed 1 month 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. 73
      src/components/GenerateCourse/UserCoursesList.tsx
  5. 71
      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({
term,
difficulty,
slug: courseSlug,
onCourseIdChange: setCourseId,
onCourseSlugChange: setCourseSlug,
onCourseChange: setCourse,

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

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

@ -10,7 +10,7 @@ import { Gift, Loader2, Search, User2 } from 'lucide-react';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import { cn } from '../../lib/classname';
import { billingDetailsOptions } from '../../queries/billing';
import { useIsPaidUser } from '../../queries/billing';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
type UserCoursesListProps = {};
@ -20,17 +20,13 @@ export function UserCoursesList(props: UserCoursesListProps) {
const [isInitialLoading, setIsInitialLoading] = useState(true);
const [showUpgradePopup, setShowUpgradePopup] = useState(false);
const { data: limits, isLoading } = useQuery(
const { data: limits, isLoading: isLimitsLoading } = useQuery(
getAiCourseLimitOptions(),
queryClient,
);
const { used, limit } = limits ?? { used: 0, limit: 0 };
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
useQuery(billingDetailsOptions(), queryClient);
const isPaidUser = userBillingDetails?.status !== 'active';
const { isPaidUser, isLoading: isPaidUserLoading } = useIsPaidUser();
const { data: userAiCourses, isFetching: isUserAiCoursesLoading } = useQuery(
listUserAiCoursesOptions(),
@ -55,13 +51,6 @@ export function UserCoursesList(props: UserCoursesListProps) {
});
const isAuthenticated = isLoggedIn();
const canSearch =
!isInitialLoading &&
!isUserAiCoursesLoading &&
isAuthenticated &&
userAiCourses?.length !== 0;
const limitUsedPercentage = Math.round((used / limit) * 100);
return (
@ -72,37 +61,39 @@ export function UserCoursesList(props: UserCoursesListProps) {
<div className="mb-3 flex min-h-[35px] items-center justify-between max-sm:mb-1">
<div className="flex items-center gap-2">
<h2 className="text-lg font-semibold">
<span className='max-md:hidden'>Your </span>Courses
<span className="max-md:hidden">Your </span>Courses
</h2>
</div>
<div className="flex items-center gap-2">
<div
className={cn(
'flex items-center gap-2 opacity-0 transition-opacity',
{
'opacity-100': !isPaidUser,
},
)}
>
<p className="flex items-center text-sm text-yellow-600">
<span className="max-md:hidden">
{limitUsedPercentage}% of daily limit used{' '}
</span>
<span className="inline md:hidden">
{limitUsedPercentage}% used
</span>
<button
onClick={() => {
setShowUpgradePopup(true);
}}
className="ml-1.5 flex items-center gap-1 rounded-full bg-yellow-600 py-0.5 pl-1.5 pr-2 text-xs text-white"
>
<Gift className="size-4" />
Upgrade
</button>
</p>
</div>
{used > 0 && limit > 0 && !isPaidUserLoading && (
<div
className={cn(
'flex items-center gap-2 opacity-0 transition-opacity',
{
'opacity-100': !isPaidUser,
},
)}
>
<p className="flex items-center text-sm text-yellow-600">
<span className="max-md:hidden">
{limitUsedPercentage}% of daily limit used{' '}
</span>
<span className="inline md:hidden">
{limitUsedPercentage}% used
</span>
<button
onClick={() => {
setShowUpgradePopup(true);
}}
className="ml-1.5 flex items-center gap-1 rounded-full bg-yellow-600 py-0.5 pl-1.5 pr-2 text-xs text-white"
>
<Gift className="size-4" />
Upgrade
</button>
</p>
</div>
)}
<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">

@ -9,10 +9,11 @@ import { getAiCourseLimitOptions } from '../queries/ai-course';
type GenerateCourseOptions = {
term: string;
difficulty: string;
slug?: string;
isForce?: boolean;
onCourseIdChange?: (courseId: string) => void;
onCourseSlugChange?: (courseSlug: string) => void;
onCourseChange?: (course: AiCourse) => void;
onCourseChange?: (course: AiCourse, rawData: string) => void;
onLoadingChange?: (isLoading: boolean) => void;
onError?: (error: string) => void;
};
@ -20,6 +21,7 @@ type GenerateCourseOptions = {
export async function generateCourse(options: GenerateCourseOptions) {
const {
term,
slug,
difficulty,
onCourseIdChange,
onCourseSlugChange,
@ -30,29 +32,47 @@ export async function generateCourse(options: GenerateCourseOptions) {
} = options;
onLoadingChange?.(true);
onCourseChange?.({
title: '',
modules: [],
difficulty: '',
});
onCourseChange?.(
{
title: '',
modules: [],
difficulty: '',
},
'',
);
onError?.('');
try {
const response = await fetch(
`${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-course`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
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',
},
body: JSON.stringify({
keyword: term,
difficulty,
isForce,
}),
credentials: 'include',
},
);
);
} else {
response = await fetch(
`${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-course`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
keyword: term,
difficulty,
isForce,
}),
credentials: 'include',
},
);
}
if (!response.ok) {
const data = await response.json();
@ -108,10 +128,13 @@ export async function generateCourse(options: GenerateCourseOptions) {
try {
const aiCourse = generateAiCourseStructure(result);
onCourseChange?.({
...aiCourse,
difficulty: difficulty || '',
});
onCourseChange?.(
{
...aiCourse,
difficulty: difficulty || '',
},
result,
);
} catch (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() {
const { data } = useQuery(
const { data, isLoading } = useQuery(
{
queryKey: ['billing-details'],
queryFn: async () => {
@ -67,7 +67,10 @@ export function useIsPaidUser() {
queryClient,
);
return data ?? false;
return {
isPaidUser: data ?? false,
isLoading,
};
}
type CoursePriceParams = {

Loading…
Cancel
Save