From 798ae0a9949796ea88cd58a92510a5488e0801e3 Mon Sep 17 00:00:00 2001 From: Arik Chakma Date: Wed, 23 Jul 2025 22:42:21 +0600 Subject: [PATCH] feat: add limits to ai courses and guides (#8900) * wip * wip * wip * wip * feat: add show upgrade modal * chore: show upgrade on generate * chore: upgrade modal * Update limits messgaes --------- Co-authored-by: Kamran Ahmed --- src/components/AIChat/AIChat.tsx | 6 +- src/components/AIQuiz/AIQuiz.tsx | 4 +- src/components/AIQuiz/AIQuizGenerator.tsx | 31 ++++++-- src/components/AIQuiz/GenerateAIQuiz.tsx | 63 ++++++++++++--- src/components/AIQuiz/UserQuizzesList.tsx | 30 +++++-- src/components/AIRoadmap/AIRoadmap.tsx | 4 +- src/components/AIRoadmap/AIRoadmapChat.tsx | 4 +- .../AIRoadmap/GenerateAIRoadmap.tsx | 62 +++++++++++++-- src/components/AIRoadmap/UserRoadmapsList.tsx | 38 +++++++-- src/components/AITutor/AITutorHeader.tsx | 8 +- src/components/AITutor/AITutorSidebar.tsx | 4 +- src/components/AITutor/UpgradeSidebarCard.tsx | 4 +- .../AIUsageWarning/AIUsageWarning.tsx | 51 ++++++++++++ src/components/Billing/GlobalUpgradeModal.tsx | 16 ++++ .../ContentGenerator/ContentGenerator.tsx | 32 ++++++-- .../FrameRenderer/RoadmapFloatingChat.tsx | 4 +- .../GenerateCourse/AICourseLesson.tsx | 7 +- .../GenerateCourse/AICourseLessonChat.tsx | 6 +- .../GenerateCourse/AICourseLimit.tsx | 7 +- .../GenerateCourse/AILimitsPopup.tsx | 7 +- .../GenerateCourse/GenerateAICourse.tsx | 79 +++++++++++++------ .../GenerateCourse/UserCoursesList.tsx | 38 +++++++-- src/components/GenerateGuide/AIGuide.tsx | 4 +- src/components/GenerateGuide/AIGuideChat.tsx | 4 +- .../GenerateGuide/GenerateAIGuide.tsx | 57 +++++++++++-- .../GenerateGuide/UserGuidesList.tsx | 30 +++++-- .../RoadmapAIChat/RoadmapAIChat.tsx | 4 +- .../RoadmapAIChat/RoadmapAIChatHeader.tsx | 4 +- src/components/TopicDetail/TopicDetailAI.tsx | 8 +- src/helper/generate-ai-course.ts | 4 +- src/helper/generate-ai-guide.ts | 4 +- src/hooks/use-roadmap-ai-chat.tsx | 6 +- src/layouts/BaseLayout.astro | 2 + src/queries/ai-course.ts | 20 ++++- src/queries/ai-quiz.ts | 4 +- src/queries/ai-roadmap.ts | 4 +- src/stores/subscription.ts | 11 +++ 37 files changed, 525 insertions(+), 146 deletions(-) create mode 100644 src/components/AIUsageWarning/AIUsageWarning.tsx create mode 100644 src/components/Billing/GlobalUpgradeModal.tsx create mode 100644 src/stores/subscription.ts diff --git a/src/components/AIChat/AIChat.tsx b/src/components/AIChat/AIChat.tsx index 86246a0c4..1213580d8 100644 --- a/src/components/AIChat/AIChat.tsx +++ b/src/components/AIChat/AIChat.tsx @@ -12,7 +12,7 @@ import { flushSync } from 'react-dom'; import AutogrowTextarea from 'react-textarea-autosize'; import { QuickHelpPrompts } from './QuickHelpPrompts'; import { QuickActionButton } from './QuickActionButton'; -import { getAiCourseLimitOptions } from '../../queries/ai-course'; +import { aiLimitOptions } from '../../queries/ai-course'; import { isLoggedIn, removeAuthToken } from '../../lib/jwt'; import { useMutation, useQuery } from '@tanstack/react-query'; import { queryClient } from '../../stores/query-client'; @@ -83,7 +83,7 @@ export function AIChat(props: AIChatProps) { const textareaMessageRef = useRef(null); const { data: tokenUsage, isLoading } = useQuery( - getAiCourseLimitOptions(), + aiLimitOptions(), queryClient, ); @@ -275,7 +275,7 @@ export function AIChat(props: AIChatProps) { setAiChatHistory(newMessages); }); - queryClient.invalidateQueries(getAiCourseLimitOptions()); + queryClient.invalidateQueries(aiLimitOptions()); queryClient.invalidateQueries({ predicate: (query) => { return query.queryKey[0] === 'list-chat-history'; diff --git a/src/components/AIQuiz/AIQuiz.tsx b/src/components/AIQuiz/AIQuiz.tsx index bd2c778b3..5c36e5f83 100644 --- a/src/components/AIQuiz/AIQuiz.tsx +++ b/src/components/AIQuiz/AIQuiz.tsx @@ -3,7 +3,7 @@ import { useState } from 'react'; import { queryClient } from '../../stores/query-client'; import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal'; import { AlertCircleIcon } from 'lucide-react'; -import { getAiCourseLimitOptions } from '../../queries/ai-course'; +import { aiLimitOptions } from '../../queries/ai-course'; import { billingDetailsOptions } from '../../queries/billing'; import { AIQuizLayout } from './AIQuizLayout'; import { GenerateAIQuiz } from './GenerateAIQuiz'; @@ -34,7 +34,7 @@ export function AIQuiz(props: AIQuizProps) { data: tokenUsage, isLoading: isTokenUsageLoading, refetch: refetchTokenUsage, - } = useQuery(getAiCourseLimitOptions(), queryClient); + } = useQuery(aiLimitOptions(), queryClient); const { data: userBillingDetails, isLoading: isBillingDetailsLoading } = useQuery(billingDetailsOptions(), queryClient); diff --git a/src/components/AIQuiz/AIQuizGenerator.tsx b/src/components/AIQuiz/AIQuizGenerator.tsx index c2b863b7d..706405838 100644 --- a/src/components/AIQuiz/AIQuizGenerator.tsx +++ b/src/components/AIQuiz/AIQuizGenerator.tsx @@ -24,7 +24,8 @@ import { getUrlParams } from '../../lib/browser'; import { FormatItem } from '../ContentGenerator/FormatItem'; import { queryClient } from '../../stores/query-client'; import { useQuery } from '@tanstack/react-query'; -import { getAiCourseLimitOptions } from '../../queries/ai-course'; +import { aiLimitOptions } from '../../queries/ai-course'; +import { showUpgradeModal } from '../../stores/subscription'; const allowedFormats = ['mcq', 'open-ended', 'mixed'] as const; export type AllowedFormat = (typeof allowedFormats)[number]; @@ -45,7 +46,7 @@ export function AIQuizGenerator() { data: tokenUsage, isLoading: isTokenUsageLoading, refetch: refetchTokenUsage, - } = useQuery(getAiCourseLimitOptions(), queryClient); + } = useQuery(aiLimitOptions(), queryClient); const { data: userBillingDetails, isLoading: isBillingDetailsLoading } = useQuery(billingDetailsOptions(), queryClient); @@ -53,6 +54,13 @@ export function AIQuizGenerator() { const isLimitExceeded = (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0); const isPaidUser = userBillingDetails?.status === 'active'; + const selectedLimit = tokenUsage?.quiz; + const showLimitWarning = + !isPaidUser && + !isBillingDetailsLoading && + !isTokenUsageLoading && + isLoggedIn(); + const titleFieldId = useId(); const fineTuneOptionsId = useId(); @@ -101,6 +109,15 @@ export function AIQuizGenerator() { return; } + if ( + !isPaidUser && + selectedLimit && + selectedLimit?.used >= selectedLimit?.limit + ) { + showUpgradeModal(); + return; + } + let sessionId = ''; if (showFineTuneOptions) { clearQuestionAnswerChatMessages(); @@ -131,14 +148,14 @@ export function AIQuizGenerator() { setIsUpgradeModalOpen(false)} /> )} - {!isPaidUser && !isBillingDetailsLoading && isLoggedIn() && ( + {showLimitWarning && (
- You are on the free plan + {selectedLimit?.used} of {selectedLimit?.limit} quizzes
)} @@ -249,7 +266,9 @@ export function AIQuizGenerator() { + + ) + ) : ( + `Sign up or login to generate your first ${type}. Takes 2s to do so.` + )} +

+ ); +} diff --git a/src/components/Billing/GlobalUpgradeModal.tsx b/src/components/Billing/GlobalUpgradeModal.tsx new file mode 100644 index 000000000..7ac08c43b --- /dev/null +++ b/src/components/Billing/GlobalUpgradeModal.tsx @@ -0,0 +1,16 @@ +import { useStore } from '@nanostores/react'; +import { + hideUpgradeModal, + isUpgradeModalOpen, +} from '../../stores/subscription'; +import { UpgradeAccountModal } from './UpgradeAccountModal'; + +export function GlobalUpgradeModal() { + const isOpen = useStore(isUpgradeModalOpen); + + if (!isOpen) { + return null; + } + + return ; +} diff --git a/src/components/ContentGenerator/ContentGenerator.tsx b/src/components/ContentGenerator/ContentGenerator.tsx index 10dbbda30..47d530f86 100644 --- a/src/components/ContentGenerator/ContentGenerator.tsx +++ b/src/components/ContentGenerator/ContentGenerator.tsx @@ -23,6 +23,10 @@ import { useToast } from '../../hooks/use-toast'; import { cn } from '../../lib/classname'; import { getUrlParams } from '../../lib/browser'; import { useParams } from '../../hooks/use-params'; +import { useQuery } from '@tanstack/react-query'; +import { aiLimitOptions } from '../../queries/ai-course'; +import { queryClient } from '../../stores/query-client'; +import { showUpgradeModal } from '../../stores/subscription'; const allowedFormats = ['course', 'guide', 'roadmap'] as const; export type AllowedFormat = (typeof allowedFormats)[number]; @@ -36,6 +40,11 @@ export function ContentGenerator() { const [title, setTitle] = useState(''); const [selectedFormat, setSelectedFormat] = useState('course'); + const { data: limits, isLoading: isLimitLoading } = useQuery( + aiLimitOptions(), + queryClient, + ); + useEffect(() => { const isValidFormat = allowedFormats.find( (format) => format.value === params.format, @@ -48,7 +57,6 @@ export function ContentGenerator() { } }, [params.format]); - // question answer chat options const [showFineTuneOptions, setShowFineTuneOptions] = useState(false); const [questionAnswerChatMessages, setQuestionAnswerChatMessages] = useState< QuestionAnswerChatMessage[] @@ -87,12 +95,25 @@ export function ContentGenerator() { }, ]; + const selectedLimit = limits?.[selectedFormat]; + const showLimitWarning = + !isPaidUser && !isPaidUserLoading && !isLimitLoading && isLoggedIn(); + const handleSubmit = () => { if (!isLoggedIn()) { showLoginPopup(); return; } + if ( + !isPaidUser && + selectedLimit && + selectedLimit?.used >= selectedLimit?.limit + ) { + showUpgradeModal(); + return; + } + let sessionId = ''; if (showFineTuneOptions) { clearQuestionAnswerChatMessages(); @@ -126,17 +147,18 @@ export function ContentGenerator() { {isUpgradeModalOpen && ( setIsUpgradeModalOpen(false)} /> )} - {!isPaidUser && !isPaidUserLoading && isLoggedIn() && ( + {showLimitWarning && (
- You are on the free plan + {selectedLimit?.used} of {selectedLimit?.limit} {selectedFormat}s
)} +

What can I help you learn?

@@ -233,7 +255,7 @@ export function ContentGenerator() {