From cfbb4f32abd4cf46cf9487234ab0e76d3b430735 Mon Sep 17 00:00:00 2001
From: Kamran Ahmed <kamranahmed.se@gmail.com>
Date: Thu, 13 Mar 2025 14:42:32 +0000
Subject: [PATCH] Refactor ai courses

---
 src/components/Activity/ProjectProgress.tsx   |   1 -
 src/components/Activity/ResourceProgress.tsx  |   2 +-
 src/components/Billing/BillingPage.tsx        |  55 ++++----
 src/components/Billing/BillingWarning.tsx     |  39 ++++++
 .../Dashboard/DashboardCustomProgressCard.tsx |   2 +-
 .../Dashboard/DashboardProgressCard.tsx       |   3 +-
 .../GenerateCourse/AICourseCard.tsx           |   2 +-
 .../GenerateCourse/AICourseContent.tsx        |  84 ++++++-----
 .../AICourseFollowUpPopover.tsx               |  10 +-
 .../GenerateCourse/AICourseLimit.tsx          |   8 +-
 .../GenerateCourse/AICourseModuleView.tsx     |   4 +-
 .../GenerateCourse/AILimitsPopup.tsx          |   2 +-
 .../GenerateCourse/GenerateAICourse.tsx       | 125 +++--------------
 src/components/GenerateCourse/GetAICourse.tsx |  26 +++-
 .../GenerateCourse/RegenerateOutline.tsx      |  75 ++++++++++
 .../GenerateCourse/UserCoursesList.tsx        |   2 +-
 src/components/GenerateCourse/re-generate     |   0
 .../GenerateRoadmap/GenerateRoadmap.tsx       |   3 +-
 .../GenerateRoadmap/RoadmapTopicDetail.tsx    |   4 +-
 src/components/Navigation/Navigation.astro    |   5 +-
 .../UserPublicProfile/UserProfileRoadmap.tsx  |   2 +-
 .../UserPublicProgressStats.tsx               |   2 +-
 .../UserPublicProgresses.tsx                  |   9 +-
 src/helper/generate-ai-course.ts              | 132 ++++++++++++++++++
 src/helper/read-stream.ts                     | 112 ---------------
 src/lib/ai.ts                                 | 114 +++++++++++++++
 src/lib/number.ts                             |  13 ++
 src/queries/ai-course.ts                      |   2 +-
 src/queries/billing.ts                        |  19 ++-
 29 files changed, 543 insertions(+), 314 deletions(-)
 create mode 100644 src/components/Billing/BillingWarning.tsx
 create mode 100644 src/components/GenerateCourse/RegenerateOutline.tsx
 create mode 100644 src/components/GenerateCourse/re-generate
 create mode 100644 src/helper/generate-ai-course.ts

diff --git a/src/components/Activity/ProjectProgress.tsx b/src/components/Activity/ProjectProgress.tsx
index 6ac20de28..3e91c25f3 100644
--- a/src/components/Activity/ProjectProgress.tsx
+++ b/src/components/Activity/ProjectProgress.tsx
@@ -1,5 +1,4 @@
 import { getUser } from '../../lib/jwt';
-import { getPercentage } from '../../helper/number';
 import { ProjectProgressActions } from './ProjectProgressActions';
 import { cn } from '../../lib/classname';
 import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions';
diff --git a/src/components/Activity/ResourceProgress.tsx b/src/components/Activity/ResourceProgress.tsx
index 28c88ec9f..00b38aba3 100644
--- a/src/components/Activity/ResourceProgress.tsx
+++ b/src/components/Activity/ResourceProgress.tsx
@@ -1,7 +1,7 @@
 import { getUser } from '../../lib/jwt';
-import { getPercentage } from '../../helper/number';
 import { ResourceProgressActions } from './ResourceProgressActions';
 import { cn } from '../../lib/classname';
+import { getPercentage } from '../../lib/number';
 
 type ResourceProgressType = {
   resourceType: 'roadmap' | 'best-practice';
diff --git a/src/components/Billing/BillingPage.tsx b/src/components/Billing/BillingPage.tsx
index fdf8287b0..92748a844 100644
--- a/src/components/Billing/BillingPage.tsx
+++ b/src/components/Billing/BillingPage.tsx
@@ -16,10 +16,11 @@ import {
   Calendar,
   RefreshCw,
   Loader2,
-  AlertTriangle,
   CreditCard,
   ArrowRightLeft,
+  CircleX,
 } from 'lucide-react';
+import { BillingWarning } from './BillingWarning';
 
 export type CreateCustomerPortalBody = {};
 
@@ -38,6 +39,10 @@ export function BillingPage() {
     queryClient,
   );
 
+  const isCanceled =
+    billingDetails?.status === 'canceled' || billingDetails?.cancelAtPeriodEnd;
+  const isPastDue = billingDetails?.status === 'past_due';
+
   const {
     mutate: createCustomerPortal,
     isSuccess: isCreatingCustomerPortalSuccess,
@@ -80,9 +85,6 @@ export function BillingPage() {
   const selectedPlanDetails = USER_SUBSCRIPTION_PLAN_PRICES.find(
     (plan) => plan.priceId === billingDetails?.priceId,
   );
-
-  const shouldHideDeleteButton =
-    billingDetails?.status === 'canceled' || billingDetails?.cancelAtPeriodEnd;
   const priceDetails = selectedPlanDetails;
 
   const formattedNextBillDate = new Date(
@@ -115,25 +117,30 @@ export function BillingPage() {
         !isLoadingBillingDetails &&
         priceDetails && (
           <div className="mt-1">
-            {billingDetails?.status === 'past_due' && (
-              <div className="mb-6 flex items-center gap-2 rounded-lg border border-red-300 bg-red-50 p-4 text-sm text-red-600">
-                <AlertTriangle className="h-5 w-5" />
-                <span>
-                  We were not able to charge your card.{' '}
-                  <button
-                    disabled={
-                      isCreatingCustomerPortal ||
-                      isCreatingCustomerPortalSuccess
-                    }
-                    onClick={() => {
-                      createCustomerPortal({});
-                    }}
-                    className="font-semibold underline underline-offset-4 disabled:cursor-not-allowed disabled:opacity-50"
-                  >
-                    Update payment information.
-                  </button>
-                </span>
-              </div>
+            {isCanceled && (
+              <BillingWarning
+                icon={CircleX}
+                message="Your subscription has been canceled."
+                buttonText="Reactivate?"
+                onButtonClick={() => {
+                  createCustomerPortal({});
+                }}
+                isLoading={
+                  isCreatingCustomerPortal || isCreatingCustomerPortalSuccess
+                }
+              />
+            )}
+            {isPastDue && (
+              <BillingWarning
+                message="We were not able to charge your card."
+                buttonText="Update payment information."
+                onButtonClick={() => {
+                  createCustomerPortal({});
+                }}
+                isLoading={
+                  isCreatingCustomerPortal || isCreatingCustomerPortalSuccess
+                }
+              />
             )}
 
             <h2 className="mb-2 text-xl font-semibold text-black">
@@ -181,7 +188,7 @@ export function BillingPage() {
               </div>
 
               <div className="mt-8 flex gap-3 max-sm:flex-col">
-                {!shouldHideDeleteButton && (
+                {!isCanceled && (
                   <button
                     className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-colors hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2 max-sm:flex-grow"
                     onClick={() => {
diff --git a/src/components/Billing/BillingWarning.tsx b/src/components/Billing/BillingWarning.tsx
new file mode 100644
index 000000000..f71cce994
--- /dev/null
+++ b/src/components/Billing/BillingWarning.tsx
@@ -0,0 +1,39 @@
+import { AlertTriangle, type LucideIcon } from 'lucide-react';
+
+export type BillingWarningProps = {
+  icon?: LucideIcon;
+  message: string;
+  onButtonClick?: () => void;
+  buttonText?: string;
+  isLoading?: boolean;
+};
+
+export function BillingWarning(props: BillingWarningProps) {
+  const {
+    message,
+    onButtonClick,
+    buttonText,
+    isLoading,
+    icon: Icon = AlertTriangle,
+  } = props;
+
+  return (
+    <div className="mb-6 flex items-center gap-2 rounded-lg border border-red-300 bg-red-50 p-4 text-sm text-red-600">
+      <Icon className="h-5 w-5" />
+      <span>
+        {message}
+        {buttonText && (
+          <button
+            disabled={isLoading}
+            onClick={() => {
+              onButtonClick?.();
+            }}
+            className="font-semibold underline underline-offset-4 disabled:cursor-not-allowed disabled:opacity-50 ml-0.5"
+          >
+            {buttonText}
+          </button>
+        )}
+      </span>
+    </div>
+  );
+}
diff --git a/src/components/Dashboard/DashboardCustomProgressCard.tsx b/src/components/Dashboard/DashboardCustomProgressCard.tsx
index 9464d5a73..9262614bc 100644
--- a/src/components/Dashboard/DashboardCustomProgressCard.tsx
+++ b/src/components/Dashboard/DashboardCustomProgressCard.tsx
@@ -1,5 +1,5 @@
-import { getPercentage } from '../../helper/number';
 import { getRelativeTimeString } from '../../lib/date';
+import { getPercentage } from '../../lib/number';
 import type { UserProgress } from '../TeamProgress/TeamProgressPage';
 
 type DashboardCustomProgressCardProps = {
diff --git a/src/components/Dashboard/DashboardProgressCard.tsx b/src/components/Dashboard/DashboardProgressCard.tsx
index d243051b0..467cffb6c 100644
--- a/src/components/Dashboard/DashboardProgressCard.tsx
+++ b/src/components/Dashboard/DashboardProgressCard.tsx
@@ -1,6 +1,5 @@
-import { getPercentage } from '../../helper/number';
+import { getPercentage } from '../../lib/number';
 import type { UserProgress } from '../TeamProgress/TeamProgressPage';
-import { ArrowUpRight, ExternalLink } from 'lucide-react';
 
 type DashboardProgressCardProps = {
   progress: UserProgress;
diff --git a/src/components/GenerateCourse/AICourseCard.tsx b/src/components/GenerateCourse/AICourseCard.tsx
index 3a4a6bd6c..4048dd73f 100644
--- a/src/components/GenerateCourse/AICourseCard.tsx
+++ b/src/components/GenerateCourse/AICourseCard.tsx
@@ -27,7 +27,7 @@ export function AICourseCard(props: AICourseCardProps) {
 
   // Calculate progress percentage
   const totalTopics = course.lessonCount || 0;
-  const completedTopics = course.progress?.done?.length || 0;
+  const completedTopics = course.done?.length || 0;
   const progressPercentage =
     totalTopics > 0 ? Math.round((completedTopics / totalTopics) * 100) : 0;
 
diff --git a/src/components/GenerateCourse/AICourseContent.tsx b/src/components/GenerateCourse/AICourseContent.tsx
index ce3ad71fb..7e30708e7 100644
--- a/src/components/GenerateCourse/AICourseContent.tsx
+++ b/src/components/GenerateCourse/AICourseContent.tsx
@@ -20,16 +20,18 @@ import { AICourseModuleList } from './AICourseModuleList';
 import { AICourseModuleView } from './AICourseModuleView';
 import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
 import { AILimitsPopup } from './AILimitsPopup';
+import { RegenerateOutline } from './RegenerateOutline';
 
 type AICourseContentProps = {
   courseSlug?: string;
   course: AiCourse;
   isLoading: boolean;
   error?: string;
+  onRegenerateOutline: () => void;
 };
 
 export function AICourseContent(props: AICourseContentProps) {
-  const { course, courseSlug, isLoading, error } = props;
+  const { course, courseSlug, isLoading, error, onRegenerateOutline } = props;
 
   const [showUpgradeModal, setShowUpgradeModal] = useState(false);
   const [showAILimitsPopup, setShowAILimitsPopup] = useState(false);
@@ -49,21 +51,23 @@ export function AICourseContent(props: AICourseContentProps) {
   >({});
 
   const goToNextModule = () => {
-    if (activeModuleIndex < course.modules.length - 1) {
-      const nextModuleIndex = activeModuleIndex + 1;
-      setActiveModuleIndex(nextModuleIndex);
-      setActiveLessonIndex(0);
-
-      setExpandedModules((prev) => {
-        const newState: Record<number, boolean> = {};
-        course.modules.forEach((_, idx) => {
-          newState[idx] = false;
-        });
-
-        newState[nextModuleIndex] = true;
-        return newState;
-      });
+    if (activeModuleIndex >= course.modules.length) {
+      return;
     }
+
+    const nextModuleIndex = activeModuleIndex + 1;
+    setActiveModuleIndex(nextModuleIndex);
+    setActiveLessonIndex(0);
+
+    setExpandedModules((prev) => {
+      const newState: Record<number, boolean> = {};
+      course.modules.forEach((_, idx) => {
+        newState[idx] = false;
+      });
+
+      newState[nextModuleIndex] = true;
+      return newState;
+    });
   };
 
   const goToNextLesson = () => {
@@ -78,26 +82,29 @@ export function AICourseContent(props: AICourseContentProps) {
   const goToPrevLesson = () => {
     if (activeLessonIndex > 0) {
       setActiveLessonIndex(activeLessonIndex - 1);
-    } else {
-      const prevModule = course.modules[activeModuleIndex - 1];
-      if (prevModule) {
-        const prevModuleIndex = activeModuleIndex - 1;
-        setActiveModuleIndex(prevModuleIndex);
-        setActiveLessonIndex(prevModule.lessons.length - 1);
-
-        // Expand the previous module in the sidebar
-        setExpandedModules((prev) => {
-          const newState: Record<number, boolean> = {};
-          // Set all modules to collapsed
-          course.modules.forEach((_, idx) => {
-            newState[idx] = false;
-          });
-          // Expand only the previous module
-          newState[prevModuleIndex] = true;
-          return newState;
-        });
-      }
+      return;
+    }
+
+    const prevModule = course.modules[activeModuleIndex - 1];
+    if (!prevModule) {
+      return;
     }
+
+    const prevModuleIndex = activeModuleIndex - 1;
+    setActiveModuleIndex(prevModuleIndex);
+    setActiveLessonIndex(prevModule.lessons.length - 1);
+
+    // Expand the previous module in the sidebar
+    setExpandedModules((prev) => {
+      const newState: Record<number, boolean> = {};
+      // Set all modules to collapsed
+      course.modules.forEach((_, idx) => {
+        newState[idx] = false;
+      });
+      // Expand only the previous module
+      newState[prevModuleIndex] = true;
+      return newState;
+    });
   };
 
   const currentModule = course.modules[activeModuleIndex];
@@ -109,6 +116,7 @@ export function AICourseContent(props: AICourseContentProps) {
     (total, module) => total + module.lessons.length,
     0,
   );
+
   const totalDoneLessons = aiCourseProgress?.done?.length || 0;
   const finishedPercentage = Math.round(
     (totalDoneLessons / totalCourseLessons) * 100,
@@ -351,7 +359,7 @@ export function AICourseContent(props: AICourseContentProps) {
             <div className="mx-auto rounded-xl border border-gray-200 bg-white shadow-sm lg:max-w-3xl">
               <div
                 className={cn(
-                  'mb-1 flex items-start justify-between border-b border-gray-100 p-6 max-lg:hidden',
+                  'relative mb-1 flex items-start justify-between border-b border-gray-100 p-6 max-lg:hidden',
                   isLoading && 'striped-loader',
                 )}
               >
@@ -363,6 +371,12 @@ export function AICourseContent(props: AICourseContentProps) {
                     {course.title ? course.difficulty : 'Please wait ..'}
                   </p>
                 </div>
+
+                {!isLoading && (
+                  <RegenerateOutline
+                    onRegenerateOutline={onRegenerateOutline}
+                  />
+                )}
               </div>
               {course.title ? (
                 <div className="flex flex-col p-6 max-lg:mt-0.5 max-lg:p-4">
diff --git a/src/components/GenerateCourse/AICourseFollowUpPopover.tsx b/src/components/GenerateCourse/AICourseFollowUpPopover.tsx
index d1e84976f..4a03fdf1c 100644
--- a/src/components/GenerateCourse/AICourseFollowUpPopover.tsx
+++ b/src/components/GenerateCourse/AICourseFollowUpPopover.tsx
@@ -2,18 +2,18 @@ import { useQuery } from '@tanstack/react-query';
 import { BookOpen, Bot, Code, HelpCircle, LockIcon, Send } from 'lucide-react';
 import { useEffect, useMemo, useRef, useState, type FormEvent } from 'react';
 import { flushSync } from 'react-dom';
+import TextareaAutosize from 'react-textarea-autosize';
 import { useOutsideClick } from '../../hooks/use-outside-click';
-import { readAICourseLessonStream } from '../../helper/read-stream';
-import { isLoggedIn, removeAuthToken } from '../../lib/jwt';
 import { useToast } from '../../hooks/use-toast';
+import { readStream } from '../../lib/ai';
+import { cn } from '../../lib/classname';
+import { isLoggedIn, removeAuthToken } from '../../lib/jwt';
 import {
   markdownToHtml,
   markdownToHtmlWithHighlighting,
 } from '../../lib/markdown';
-import { cn } from '../../lib/classname';
 import { getAiCourseLimitOptions } from '../../queries/ai-course';
 import { queryClient } from '../../stores/query-client';
-import TextareaAutosize from 'react-textarea-autosize';
 
 export type AllowedAIChatRole = 'user' | 'assistant';
 export type AIChatHistoryType = {
@@ -142,7 +142,7 @@ export function AICourseFollowUpPopover(props: AICourseFollowUpPopoverProps) {
       return;
     }
 
-    await readAICourseLessonStream(reader, {
+    await readStream(reader, {
       onStream: async (content) => {
         flushSync(() => {
           setStreamedMessage(content);
diff --git a/src/components/GenerateCourse/AICourseLimit.tsx b/src/components/GenerateCourse/AICourseLimit.tsx
index db57beda3..5dac1a284 100644
--- a/src/components/GenerateCourse/AICourseLimit.tsx
+++ b/src/components/GenerateCourse/AICourseLimit.tsx
@@ -1,9 +1,9 @@
 import { useQuery } from '@tanstack/react-query';
+import { Gift, Info } from 'lucide-react';
+import { getPercentage } from '../../lib/number';
 import { getAiCourseLimitOptions } from '../../queries/ai-course';
-import { queryClient } from '../../stores/query-client';
 import { billingDetailsOptions } from '../../queries/billing';
-import { getPercentage } from '../../helper/number';
-import { Gift, Info } from 'lucide-react';
+import { queryClient } from '../../stores/query-client';
 
 type AICourseLimitProps = {
   onUpgrade: () => void;
@@ -33,7 +33,7 @@ export function AICourseLimit(props: AICourseLimitProps) {
 
   // has consumed 80% of the limit
   const isNearLimit = used >= limit * 0.8;
-  const isPaidUser = userBillingDetails.status !== 'none';
+  const isPaidUser = userBillingDetails.status === 'active';
 
   return (
     <>
diff --git a/src/components/GenerateCourse/AICourseModuleView.tsx b/src/components/GenerateCourse/AICourseModuleView.tsx
index 34e07a91b..735d8d3c8 100644
--- a/src/components/GenerateCourse/AICourseModuleView.tsx
+++ b/src/components/GenerateCourse/AICourseModuleView.tsx
@@ -8,7 +8,7 @@ import {
   XIcon,
 } from 'lucide-react';
 import { useEffect, useMemo, useState } from 'react';
-import { readAICourseLessonStream } from '../../helper/read-stream';
+import { readStream } from '../../lib/ai';
 import { cn } from '../../lib/classname';
 import { isLoggedIn, removeAuthToken } from '../../lib/jwt';
 import {
@@ -136,7 +136,7 @@ export function AICourseModuleView(props: AICourseModuleViewProps) {
 
     setIsLoading(false);
     setIsGenerating(true);
-    await readAICourseLessonStream(reader, {
+    await readStream(reader, {
       onStream: async (result) => {
         if (abortController.signal.aborted) {
           return;
diff --git a/src/components/GenerateCourse/AILimitsPopup.tsx b/src/components/GenerateCourse/AILimitsPopup.tsx
index 7c87c6009..79244940c 100644
--- a/src/components/GenerateCourse/AILimitsPopup.tsx
+++ b/src/components/GenerateCourse/AILimitsPopup.tsx
@@ -24,7 +24,7 @@ export function AILimitsPopup(props: AILimitsPopupProps) {
   const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
     useQuery(billingDetailsOptions(), queryClient);
 
-  const isPaidUser = userBillingDetails?.status !== 'none';
+  const isPaidUser = userBillingDetails?.status === 'active';
 
   return (
     <Modal
diff --git a/src/components/GenerateCourse/GenerateAICourse.tsx b/src/components/GenerateCourse/GenerateAICourse.tsx
index 5822a7855..a4af327bf 100644
--- a/src/components/GenerateCourse/GenerateAICourse.tsx
+++ b/src/components/GenerateCourse/GenerateAICourse.tsx
@@ -1,11 +1,9 @@
 import { useEffect, useState } from 'react';
 import { getUrlParams } from '../../lib/browser';
 import { isLoggedIn } from '../../lib/jwt';
-import { generateAiCourseStructure, type AiCourse } from '../../lib/ai';
-import { readAICourseStream } from '../../helper/read-stream';
+import { type AiCourse } from '../../lib/ai';
 import { AICourseContent } from './AICourseContent';
-import { queryClient } from '../../stores/query-client';
-import { getAiCourseLimitOptions } from '../../queries/ai-course';
+import { generateCourse } from '../../helper/generate-ai-course';
 
 type GenerateAICourseProps = {};
 
@@ -38,119 +36,31 @@ export function GenerateAICourse(props: GenerateAICourseProps) {
 
     setTerm(paramsTerm);
     setDifficulty(paramsDifficulty);
-    generateCourse({ term: paramsTerm, difficulty: paramsDifficulty });
+    handleGenerateCourse({ term: paramsTerm, difficulty: paramsDifficulty });
   }, [term, difficulty]);
 
-  const generateCourse = async (options: {
+  const handleGenerateCourse = async (options: {
     term: string;
     difficulty: string;
+    isForce?: boolean;
   }) => {
-    const { term, difficulty } = options;
+    const { term, difficulty, isForce } = options;
 
     if (!isLoggedIn()) {
       window.location.href = '/ai-tutor';
       return;
     }
 
-    setIsLoading(true);
-    setCourse({
-      title: '',
-      modules: [],
-      difficulty: '',
+    await generateCourse({
+      term,
+      difficulty,
+      onCourseIdChange: setCourseId,
+      onCourseSlugChange: setCourseSlug,
+      onCourseChange: setCourse,
+      onLoadingChange: setIsLoading,
+      onError: setError,
+      isForce,
     });
-    setError('');
-
-    try {
-      const 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,
-          }),
-          credentials: 'include',
-        },
-      );
-
-      if (!response.ok) {
-        const data = await response.json();
-        console.error(
-          'Error generating course:',
-          data?.message || 'Something went wrong',
-        );
-        setIsLoading(false);
-        setError(data?.message || 'Something went wrong');
-        return;
-      }
-
-      const reader = response.body?.getReader();
-
-      if (!reader) {
-        console.error('Failed to get reader from response');
-        setError('Something went wrong');
-        setIsLoading(false);
-        return;
-      }
-
-      const COURSE_ID_REGEX = new RegExp('@COURSEID:(\\w+)@');
-      const COURSE_SLUG_REGEX = new RegExp(/@COURSESLUG:([\w-]+)@/);
-
-      await readAICourseStream(reader, {
-        onStream: (result) => {
-          if (result.includes('@COURSEID') || result.includes('@COURSESLUG')) {
-            const courseIdMatch = result.match(COURSE_ID_REGEX);
-            const courseSlugMatch = result.match(COURSE_SLUG_REGEX);
-            const extractedCourseId = courseIdMatch?.[1] || '';
-            const extractedCourseSlug = courseSlugMatch?.[1] || '';
-
-            if (extractedCourseSlug) {
-              window.history.replaceState(
-                {
-                  courseId,
-                  courseSlug: extractedCourseSlug,
-                  term,
-                  difficulty,
-                },
-                '',
-                `${origin}/ai-tutor/${extractedCourseSlug}`,
-              );
-            }
-
-            result = result
-              .replace(COURSE_ID_REGEX, '')
-              .replace(COURSE_SLUG_REGEX, '');
-
-            setCourseId(extractedCourseId);
-            setCourseSlug(extractedCourseSlug);
-          }
-
-          try {
-            const aiCourse = generateAiCourseStructure(result);
-            setCourse({
-              ...aiCourse,
-              difficulty: difficulty || '',
-            });
-          } catch (e) {
-            console.error('Error parsing streamed course content:', e);
-          }
-        },
-        onStreamEnd: (result) => {
-          result = result
-            .replace(COURSE_ID_REGEX, '')
-            .replace(COURSE_SLUG_REGEX, '');
-          setIsLoading(false);
-          queryClient.invalidateQueries(getAiCourseLimitOptions());
-        },
-      });
-    } catch (error: any) {
-      setError(error?.message || 'Something went wrong');
-      console.error('Error in course generation:', error);
-      setIsLoading(false);
-    }
   };
 
   useEffect(() => {
@@ -167,7 +77,7 @@ export function GenerateAICourse(props: GenerateAICourseProps) {
       setDifficulty(difficulty);
 
       setIsLoading(true);
-      generateCourse({ term, difficulty }).finally(() => {
+      handleGenerateCourse({ term, difficulty }).finally(() => {
         setIsLoading(false);
       });
     };
@@ -184,6 +94,9 @@ export function GenerateAICourse(props: GenerateAICourseProps) {
       course={course}
       isLoading={isLoading}
       error={error}
+      onRegenerateOutline={() => {
+        handleGenerateCourse({ term, difficulty, isForce: true });
+      }}
     />
   );
 }
diff --git a/src/components/GenerateCourse/GetAICourse.tsx b/src/components/GenerateCourse/GetAICourse.tsx
index d26f613c8..5eb38999f 100644
--- a/src/components/GenerateCourse/GetAICourse.tsx
+++ b/src/components/GenerateCourse/GetAICourse.tsx
@@ -5,6 +5,7 @@ import { useEffect, useState } from 'react';
 import { AICourseContent } from './AICourseContent';
 import { generateAiCourseStructure } from '../../lib/ai';
 import { isLoggedIn } from '../../lib/jwt';
+import { generateCourse } from '../../helper/generate-ai-course';
 
 type GetAICourseProps = {
   courseSlug: string;
@@ -14,7 +15,8 @@ export function GetAICourse(props: GetAICourseProps) {
   const { courseSlug } = props;
 
   const [isLoading, setIsLoading] = useState(true);
-  const { data: aiCourse, error } = useQuery(
+  const [error, setError] = useState('');
+  const { data: aiCourse, error: queryError } = useQuery(
     {
       ...getAiCourseOptions({ aiCourseSlug: courseSlug }),
       select: (data) => {
@@ -43,12 +45,27 @@ export function GetAICourse(props: GetAICourseProps) {
   }, [aiCourse]);
 
   useEffect(() => {
-    if (!error) {
+    if (!queryError) {
       return;
     }
 
     setIsLoading(false);
-  }, [error]);
+    setError(queryError.message);
+  }, [queryError]);
+
+  const handleRegenerateCourse = async () => {
+    if (!aiCourse) {
+      return;
+    }
+
+    await generateCourse({
+      term: aiCourse.keyword,
+      difficulty: aiCourse.difficulty,
+      onLoadingChange: setIsLoading,
+      onError: setError,
+      isForce: true,
+    });
+  };
 
   return (
     <AICourseContent
@@ -59,7 +76,8 @@ export function GetAICourse(props: GetAICourseProps) {
       }}
       isLoading={isLoading}
       courseSlug={courseSlug}
-      error={error?.message}
+      error={error}
+      onRegenerateOutline={handleRegenerateCourse}
     />
   );
 }
diff --git a/src/components/GenerateCourse/RegenerateOutline.tsx b/src/components/GenerateCourse/RegenerateOutline.tsx
new file mode 100644
index 000000000..0bce70b0e
--- /dev/null
+++ b/src/components/GenerateCourse/RegenerateOutline.tsx
@@ -0,0 +1,75 @@
+import { PenSquare, RefreshCcw } from 'lucide-react';
+import { useRef, useState } from 'react';
+import { useOutsideClick } from '../../hooks/use-outside-click';
+import { cn } from '../../lib/classname';
+import { useIsPaidUser } from '../../queries/billing';
+import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
+
+type RegenerateOutlineProps = {
+  onRegenerateOutline: () => void;
+};
+
+export function RegenerateOutline(props: RegenerateOutlineProps) {
+  const { onRegenerateOutline } = props;
+
+  const [isDropdownVisible, setIsDropdownVisible] = useState(false);
+  const [showUpgradeModal, setShowUpgradeModal] = useState(false);
+  const ref = useRef<HTMLDivElement>(null);
+
+  const isPaidUser = useIsPaidUser();
+
+  useOutsideClick(ref, () => setIsDropdownVisible(false));
+
+  return (
+    <>
+      {showUpgradeModal && (
+        <UpgradeAccountModal
+          onClose={() => {
+            setShowUpgradeModal(false);
+          }}
+        />
+      )}
+
+      <div className="absolute right-3 top-3" ref={ref}>
+        <button
+          className={cn('text-gray-400 hover:text-black', {
+            'text-black': isDropdownVisible,
+          })}
+          onClick={() => setIsDropdownVisible(!isDropdownVisible)}
+        >
+          <PenSquare className="text-current" size={16} strokeWidth={2.5} />
+        </button>
+        {isDropdownVisible && (
+          <div className="absolute right-0 top-full min-w-[170px] overflow-hidden rounded-md border border-gray-200 bg-white">
+            <button
+              onClick={() => {
+                if (!isPaidUser) {
+                  setIsDropdownVisible(false);
+                  setShowUpgradeModal(true);
+                } else {
+                  onRegenerateOutline();
+                }
+              }}
+              className="flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm text-gray-600 hover:bg-gray-100"
+            >
+              <RefreshCcw
+                size={16}
+                className="text-gray-400"
+                strokeWidth={2.5}
+              />
+              Regenerate
+            </button>
+            <button className="flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm text-gray-600 hover:bg-gray-100">
+              <PenSquare
+                size={16}
+                className="text-gray-400"
+                strokeWidth={2.5}
+              />
+              Modify Prompt
+            </button>
+          </div>
+        )}
+      </div>
+    </>
+  );
+}
diff --git a/src/components/GenerateCourse/UserCoursesList.tsx b/src/components/GenerateCourse/UserCoursesList.tsx
index e6e10a50d..925c882f5 100644
--- a/src/components/GenerateCourse/UserCoursesList.tsx
+++ b/src/components/GenerateCourse/UserCoursesList.tsx
@@ -30,7 +30,7 @@ export function UserCoursesList(props: UserCoursesListProps) {
   const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
     useQuery(billingDetailsOptions(), queryClient);
 
-  const isPaidUser = userBillingDetails?.status !== 'none';
+  const isPaidUser = userBillingDetails?.status !== 'active';
 
   const { data: userAiCourses, isFetching: isUserAiCoursesLoading } = useQuery(
     listUserAiCoursesOptions(),
diff --git a/src/components/GenerateCourse/re-generate b/src/components/GenerateCourse/re-generate
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/components/GenerateRoadmap/GenerateRoadmap.tsx b/src/components/GenerateRoadmap/GenerateRoadmap.tsx
index ccb2cf16f..945fc2a35 100644
--- a/src/components/GenerateRoadmap/GenerateRoadmap.tsx
+++ b/src/components/GenerateRoadmap/GenerateRoadmap.tsx
@@ -11,7 +11,6 @@ import { useToast } from '../../hooks/use-toast';
 import { generateAIRoadmapFromText } from '../../../editor/utils/roadmap-generator';
 import { renderFlowJSON } from '../../../editor/renderer/renderer';
 import { replaceChildren } from '../../lib/dom';
-import { readAIRoadmapStream } from '../../helper/read-stream';
 import {
   getOpenAIKey,
   isLoggedIn,
@@ -31,7 +30,7 @@ import { showLoginPopup } from '../../lib/popup.ts';
 import { cn } from '../../lib/classname.ts';
 import { RoadmapTopicDetail } from './RoadmapTopicDetail.tsx';
 import { AIRoadmapAlert } from './AIRoadmapAlert.tsx';
-import { IS_KEY_ONLY_ROADMAP_GENERATION } from '../../lib/ai.ts';
+import { IS_KEY_ONLY_ROADMAP_GENERATION, readAIRoadmapStream } from '../../lib/ai.ts';
 import { AITermSuggestionInput } from './AITermSuggestionInput.tsx';
 import { IncreaseRoadmapLimit } from './IncreaseRoadmapLimit.tsx';
 import { AuthenticationForm } from '../AuthenticationFlow/AuthenticationForm.tsx';
diff --git a/src/components/GenerateRoadmap/RoadmapTopicDetail.tsx b/src/components/GenerateRoadmap/RoadmapTopicDetail.tsx
index 560d9e940..33938800a 100644
--- a/src/components/GenerateRoadmap/RoadmapTopicDetail.tsx
+++ b/src/components/GenerateRoadmap/RoadmapTopicDetail.tsx
@@ -3,13 +3,13 @@ import { useEffect, useMemo, useRef, useState } from 'react';
 import { useKeydown } from '../../hooks/use-keydown';
 import { useOutsideClick } from '../../hooks/use-outside-click';
 import { markdownToHtml } from '../../lib/markdown';
-import { Ban, Cog, Contact, FileText, User, UserRound, X } from 'lucide-react';
+import { Ban, Cog, Contact, FileText, X } from 'lucide-react';
 import { Spinner } from '../ReactIcons/Spinner';
 import type { RoadmapNodeDetails } from './GenerateRoadmap';
 import { getOpenAIKey, isLoggedIn, removeAuthToken } from '../../lib/jwt';
-import { readAIRoadmapContentStream } from '../../helper/read-stream';
 import { cn } from '../../lib/classname';
 import { showLoginPopup } from '../../lib/popup';
+import { readAIRoadmapContentStream } from '../../lib/ai';
 
 type RoadmapTopicDetailProps = RoadmapNodeDetails & {
   onClose?: () => void;
diff --git a/src/components/Navigation/Navigation.astro b/src/components/Navigation/Navigation.astro
index 1b87eefd4..bebe897f9 100644
--- a/src/components/Navigation/Navigation.astro
+++ b/src/components/Navigation/Navigation.astro
@@ -49,7 +49,10 @@ import { CourseAnnouncement } from '../SQLCourse/CourseAnnouncement';
             </span>
           </span>
         </a>
-        <a href='/teams' class='group hidden xl:block relative text-gray-400 hover:text-white'>
+        <a
+          href='/teams'
+          class='group relative hidden text-gray-400 hover:text-white xl:block'
+        >
           Teams
         </a>
       </div>
diff --git a/src/components/UserPublicProfile/UserProfileRoadmap.tsx b/src/components/UserPublicProfile/UserProfileRoadmap.tsx
index 0d42ba79d..5dfb69ab4 100644
--- a/src/components/UserPublicProfile/UserProfileRoadmap.tsx
+++ b/src/components/UserPublicProfile/UserProfileRoadmap.tsx
@@ -2,7 +2,7 @@ import type {
   GetUserProfileRoadmapResponse,
   GetPublicProfileResponse,
 } from '../../api/user';
-import { getPercentage } from '../../helper/number';
+import { getPercentage } from '../../lib/number';
 import { PrivateProfileBanner } from './PrivateProfileBanner';
 import { UserProfileRoadmapRenderer } from './UserProfileRoadmapRenderer';
 
diff --git a/src/components/UserPublicProfile/UserPublicProgressStats.tsx b/src/components/UserPublicProfile/UserPublicProgressStats.tsx
index 9b8fc85f5..c9eef634c 100644
--- a/src/components/UserPublicProfile/UserPublicProgressStats.tsx
+++ b/src/components/UserPublicProfile/UserPublicProgressStats.tsx
@@ -1,5 +1,5 @@
-import { getPercentage } from '../../helper/number';
 import { getRelativeTimeString } from '../../lib/date';
+import { getPercentage } from '../../lib/number';
 
 type UserPublicProgressStats = {
   resourceType: 'roadmap';
diff --git a/src/components/UserPublicProfile/UserPublicProgresses.tsx b/src/components/UserPublicProfile/UserPublicProgresses.tsx
index 1eac8e296..cb5fb2fb1 100644
--- a/src/components/UserPublicProfile/UserPublicProgresses.tsx
+++ b/src/components/UserPublicProfile/UserPublicProgresses.tsx
@@ -1,6 +1,5 @@
 import type { GetPublicProfileResponse } from '../../api/user';
-import { UserPublicProgressStats } from './UserPublicProgressStats';
-import { getPercentage } from '../../helper/number.ts';
+import { getPercentage } from '../../lib/number';
 
 type UserPublicProgressesProps = {
   userId: string;
@@ -73,15 +72,15 @@ export function UserPublicProgresses(props: UserPublicProgressesProps) {
                   target="_blank"
                   key={roadmap.id + counter}
                   href={`/${roadmap.id}?s=${userId}`}
-                  className="relative group border-gray-300 flex items-center justify-between rounded-md border bg-white px-3 py-2 text-left text-sm transition-all hover:border-gray-400 overflow-hidden"
+                  className="group relative flex items-center justify-between overflow-hidden rounded-md border border-gray-300 bg-white px-3 py-2 text-left text-sm transition-all hover:border-gray-400"
                 >
                   <span className="flex-grow truncate">{roadmap.title}</span>
                   <span className="text-xs text-gray-400">
-                    {parseInt(percentageDone, 10)}%
+                    {percentageDone}%
                   </span>
 
                   <span
-                    className="absolute transition-colors left-0 top-0 block h-full cursor-pointer rounded-tl-md bg-black/5 group-hover:bg-black/10"
+                    className="absolute left-0 top-0 block h-full cursor-pointer rounded-tl-md bg-black/5 transition-colors group-hover:bg-black/10"
                     style={{
                       width: `${percentageDone}%`,
                     }}
diff --git a/src/helper/generate-ai-course.ts b/src/helper/generate-ai-course.ts
new file mode 100644
index 000000000..34092fbb1
--- /dev/null
+++ b/src/helper/generate-ai-course.ts
@@ -0,0 +1,132 @@
+import {
+  generateAiCourseStructure,
+  readStream,
+  type AiCourse,
+} from '../lib/ai';
+import { queryClient } from '../stores/query-client';
+import { getAiCourseLimitOptions } from '../queries/ai-course';
+
+type GenerateCourseOptions = {
+  term: string;
+  difficulty: string;
+  isForce?: boolean;
+  onCourseIdChange?: (courseId: string) => void;
+  onCourseSlugChange?: (courseSlug: string) => void;
+  onCourseChange?: (course: AiCourse) => void;
+  onLoadingChange?: (isLoading: boolean) => void;
+  onError?: (error: string) => void;
+};
+
+export async function generateCourse(options: GenerateCourseOptions) {
+  const {
+    term,
+    difficulty,
+    onCourseIdChange,
+    onCourseSlugChange,
+    onCourseChange,
+    onLoadingChange,
+    onError,
+    isForce = false,
+  } = options;
+
+  onLoadingChange?.(true);
+  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',
+        },
+        body: JSON.stringify({
+          keyword: term,
+          difficulty,
+          isForce,
+        }),
+        credentials: 'include',
+      },
+    );
+
+    if (!response.ok) {
+      const data = await response.json();
+      console.error(
+        'Error generating course:',
+        data?.message || 'Something went wrong',
+      );
+      onLoadingChange?.(false);
+      onError?.(data?.message || 'Something went wrong');
+      return;
+    }
+
+    const reader = response.body?.getReader();
+
+    if (!reader) {
+      console.error('Failed to get reader from response');
+      onError?.('Something went wrong');
+      onLoadingChange?.(false);
+      return;
+    }
+
+    const COURSE_ID_REGEX = new RegExp('@COURSEID:(\\w+)@');
+    const COURSE_SLUG_REGEX = new RegExp(/@COURSESLUG:([\w-]+)@/);
+
+    await readStream(reader, {
+      onStream: (result) => {
+        if (result.includes('@COURSEID') || result.includes('@COURSESLUG')) {
+          const courseIdMatch = result.match(COURSE_ID_REGEX);
+          const courseSlugMatch = result.match(COURSE_SLUG_REGEX);
+          const extractedCourseId = courseIdMatch?.[1] || '';
+          const extractedCourseSlug = courseSlugMatch?.[1] || '';
+
+          if (extractedCourseSlug) {
+            window.history.replaceState(
+              {
+                courseId: extractedCourseId,
+                courseSlug: extractedCourseSlug,
+                term,
+                difficulty,
+              },
+              '',
+              `${origin}/ai-tutor/${extractedCourseSlug}`,
+            );
+          }
+
+          result = result
+            .replace(COURSE_ID_REGEX, '')
+            .replace(COURSE_SLUG_REGEX, '');
+
+          onCourseIdChange?.(extractedCourseId);
+          onCourseSlugChange?.(extractedCourseSlug);
+        }
+
+        try {
+          const aiCourse = generateAiCourseStructure(result);
+          onCourseChange?.({
+            ...aiCourse,
+            difficulty: difficulty || '',
+          });
+        } catch (e) {
+          console.error('Error parsing streamed course content:', e);
+        }
+      },
+      onStreamEnd: (result) => {
+        result = result
+          .replace(COURSE_ID_REGEX, '')
+          .replace(COURSE_SLUG_REGEX, '');
+        onLoadingChange?.(false);
+        queryClient.invalidateQueries(getAiCourseLimitOptions());
+      },
+    });
+  } catch (error: any) {
+    onError?.(error?.message || 'Something went wrong');
+    console.error('Error in course generation:', error);
+    onLoadingChange?.(false);
+  }
+}
diff --git a/src/helper/read-stream.ts b/src/helper/read-stream.ts
index 2d65de068..8ae446cae 100644
--- a/src/helper/read-stream.ts
+++ b/src/helper/read-stream.ts
@@ -1,117 +1,5 @@
 const NEW_LINE = '\n'.charCodeAt(0);
 
-export async function readAIRoadmapStream(
-  reader: ReadableStreamDefaultReader<Uint8Array>,
-  {
-    onStream,
-    onStreamEnd,
-  }: {
-    onStream?: (roadmap: string) => void;
-    onStreamEnd?: (roadmap: string) => void;
-  },
-) {
-  const decoder = new TextDecoder('utf-8');
-  let result = '';
-
-  while (true) {
-    const { value, done } = await reader.read();
-    if (done) {
-      break;
-    }
-
-    // We will call the renderRoadmap callback whenever we encounter
-    // a new line with the result until the new line
-    // otherwise, we will keep appending the result to the previous result
-    if (value) {
-      let start = 0;
-      for (let i = 0; i < value.length; i++) {
-        if (value[i] === NEW_LINE) {
-          result += decoder.decode(value.slice(start, i + 1));
-          onStream?.(result);
-          start = i + 1;
-        }
-      }
-      if (start < value.length) {
-        result += decoder.decode(value.slice(start));
-      }
-    }
-  }
-
-  onStream?.(result);
-  onStreamEnd?.(result);
-  reader.releaseLock();
-}
-
-export async function readAIRoadmapContentStream(
-  reader: ReadableStreamDefaultReader<Uint8Array>,
-  {
-    onStream,
-    onStreamEnd,
-  }: {
-    onStream?: (roadmap: string) => void;
-    onStreamEnd?: (roadmap: string) => void;
-  },
-) {
-  const decoder = new TextDecoder('utf-8');
-  let result = '';
-
-  while (true) {
-    const { value, done } = await reader.read();
-    if (done) {
-      break;
-    }
-
-    if (value) {
-      result += decoder.decode(value);
-      onStream?.(result);
-    }
-  }
-
-  onStream?.(result);
-  onStreamEnd?.(result);
-  reader.releaseLock();
-}
-
-export async function readAICourseStream(
-  reader: ReadableStreamDefaultReader<Uint8Array>,
-  {
-    onStream,
-    onStreamEnd,
-  }: {
-    onStream?: (course: string) => void;
-    onStreamEnd?: (course: string) => void;
-  },
-) {
-  const decoder = new TextDecoder('utf-8');
-  let result = '';
-
-  while (true) {
-    const { value, done } = await reader.read();
-    if (done) {
-      break;
-    }
-
-    // Process the stream data as it comes in
-    if (value) {
-      let start = 0;
-      for (let i = 0; i < value.length; i++) {
-        if (value[i] === NEW_LINE) {
-          result += decoder.decode(value.slice(start, i + 1));
-          onStream?.(result);
-          start = i + 1;
-        }
-      }
-      if (start < value.length) {
-        result += decoder.decode(value.slice(start));
-      }
-    }
-  }
-
-  onStream?.(result);
-  onStreamEnd?.(result);
-  reader.releaseLock();
-}
-
 export async function readAICourseLessonStream(
   reader: ReadableStreamDefaultReader<Uint8Array>,
   {
diff --git a/src/lib/ai.ts b/src/lib/ai.ts
index 75307d36f..b7cec3f57 100644
--- a/src/lib/ai.ts
+++ b/src/lib/ai.ts
@@ -53,3 +53,117 @@ export function generateAiCourseStructure(
     modules,
   };
 }
+
+const NEW_LINE = '\n'.charCodeAt(0);
+
+export async function readAIRoadmapStream(
+  reader: ReadableStreamDefaultReader<Uint8Array>,
+  {
+    onStream,
+    onStreamEnd,
+  }: {
+    onStream?: (roadmap: string) => void;
+    onStreamEnd?: (roadmap: string) => void;
+  },
+) {
+  const decoder = new TextDecoder('utf-8');
+  let result = '';
+
+  while (true) {
+    const { value, done } = await reader.read();
+    if (done) {
+      break;
+    }
+
+    // We will call the renderRoadmap callback whenever we encounter
+    // a new line with the result until the new line
+    // otherwise, we will keep appending the result to the previous result
+    if (value) {
+      let start = 0;
+      for (let i = 0; i < value.length; i++) {
+        if (value[i] === NEW_LINE) {
+          result += decoder.decode(value.slice(start, i + 1));
+          onStream?.(result);
+          start = i + 1;
+        }
+      }
+      if (start < value.length) {
+        result += decoder.decode(value.slice(start));
+      }
+    }
+  }
+
+  onStream?.(result);
+  onStreamEnd?.(result);
+  reader.releaseLock();
+}
+
+export async function readAIRoadmapContentStream(
+  reader: ReadableStreamDefaultReader<Uint8Array>,
+  {
+    onStream,
+    onStreamEnd,
+  }: {
+    onStream?: (roadmap: string) => void;
+    onStreamEnd?: (roadmap: string) => void;
+  },
+) {
+  const decoder = new TextDecoder('utf-8');
+  let result = '';
+
+  while (true) {
+    const { value, done } = await reader.read();
+    if (done) {
+      break;
+    }
+
+    if (value) {
+      result += decoder.decode(value);
+      onStream?.(result);
+    }
+  }
+
+  onStream?.(result);
+  onStreamEnd?.(result);
+  reader.releaseLock();
+}
+
+export async function readStream(
+  reader: ReadableStreamDefaultReader<Uint8Array>,
+  {
+    onStream,
+    onStreamEnd,
+  }: {
+    onStream?: (course: string) => void;
+    onStreamEnd?: (course: string) => void;
+  },
+) {
+  const decoder = new TextDecoder('utf-8');
+  let result = '';
+
+  while (true) {
+    const { value, done } = await reader.read();
+    if (done) {
+      break;
+    }
+
+    // Process the stream data as it comes in
+    if (value) {
+      let start = 0;
+      for (let i = 0; i < value.length; i++) {
+        if (value[i] === NEW_LINE) {
+          result += decoder.decode(value.slice(start, i + 1));
+          onStream?.(result);
+          start = i + 1;
+        }
+      }
+      if (start < value.length) {
+        result += decoder.decode(value.slice(start));
+      }
+    }
+  }
+
+  onStream?.(result);
+  onStreamEnd?.(result);
+  reader.releaseLock();
+}
diff --git a/src/lib/number.ts b/src/lib/number.ts
index d686a5934..feadfbaf2 100644
--- a/src/lib/number.ts
+++ b/src/lib/number.ts
@@ -21,3 +21,16 @@ export function humanizeNumber(number: number): string {
 
   return `${decimalIfNeeded(number / 1000000)}m`;
 }
+
+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);
+}
diff --git a/src/queries/ai-course.ts b/src/queries/ai-course.ts
index f7a947579..c95c77154 100644
--- a/src/queries/ai-course.ts
+++ b/src/queries/ai-course.ts
@@ -39,6 +39,7 @@ export interface AICourseDocument {
   title: string;
   slug?: string;
   keyword: string;
+  done: string[];
   difficulty: string;
   data: string;
   viewCount: number;
@@ -75,7 +76,6 @@ export function getAiCourseLimitOptions() {
 }
 
 export type AICourseListItem = AICourseDocument & {
-  progress: AICourseProgressDocument;
   lessonCount: number;
 };
 
diff --git a/src/queries/billing.ts b/src/queries/billing.ts
index f7250e62a..717b8a36c 100644
--- a/src/queries/billing.ts
+++ b/src/queries/billing.ts
@@ -1,6 +1,7 @@
-import { queryOptions } from '@tanstack/react-query';
+import { queryOptions, useQuery } from '@tanstack/react-query';
 import { httpGet } from '../lib/query-http';
 import { isLoggedIn } from '../lib/jwt';
+import { queryClient } from '../stores/query-client';
 
 export const allowedSubscriptionStatus = [
   'active',
@@ -53,6 +54,22 @@ export function billingDetailsOptions() {
   });
 }
 
+export function useIsPaidUser() {
+  const { data } = useQuery(
+    {
+      queryKey: ['billing-details'],
+      queryFn: async () => {
+        return httpGet<BillingDetailsResponse>('/v1-billing-details');
+      },
+      enabled: !!isLoggedIn(),
+      select: (data) => data.status === 'active',
+    },
+    queryClient,
+  );
+
+  return data ?? false;
+}
+
 type CoursePriceParams = {
   courseSlug: string;
 };