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() {