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 <kamranahmed.se@gmail.com>
This commit is contained in:
Arik Chakma
2025-07-23 22:42:21 +06:00
committed by GitHub
parent e426b8bda8
commit 798ae0a994
37 changed files with 525 additions and 146 deletions

View File

@@ -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<HTMLTextAreaElement>(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';

View File

@@ -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);

View File

@@ -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() {
<UpgradeAccountModal onClose={() => setIsUpgradeModalOpen(false)} />
)}
{!isPaidUser && !isBillingDetailsLoading && isLoggedIn() && (
{showLimitWarning && (
<div className="absolute bottom-full left-1/2 -translate-x-1/2 -translate-y-8 text-gray-500 max-md:hidden">
You are on the free plan
{selectedLimit?.used} of {selectedLimit?.limit} quizzes
<button
onClick={() => setIsUpgradeModalOpen(true)}
className="ml-2 rounded-xl bg-yellow-600 px-2 py-1 text-sm text-white hover:opacity-80"
>
Upgrade to Pro
Need more? Upgrade
</button>
</div>
)}
@@ -249,7 +266,9 @@ export function AIQuizGenerator() {
<button
type="submit"
className="flex h-[56px] w-full items-center justify-center gap-2 rounded-xl bg-black p-4 text-white focus:outline-none disabled:cursor-not-allowed disabled:opacity-80"
disabled={!canGenerate}
disabled={
!canGenerate || isTokenUsageLoading || isBillingDetailsLoading
}
>
<SparklesIcon className="size-4" />
Generate Quiz

View File

@@ -12,6 +12,10 @@ import {
import { queryClient } from '../../stores/query-client';
import { AIQuizContent } from './AIQuizContent';
import { AlertCircleIcon } from 'lucide-react';
import { useIsPaidUser } from '../../queries/billing';
import { useQuery } from '@tanstack/react-query';
import { aiLimitOptions } from '../../queries/ai-course';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
type GenerateAIQuizProps = {
onQuizSlugChange?: (quizSlug: string) => void;
@@ -27,7 +31,27 @@ export function GenerateAIQuiz(props: GenerateAIQuizProps) {
const [questions, setQuestions] = useState<QuizQuestion[]>([]);
const questionsRef = useRef<QuizQuestion[]>([]);
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
const { isPaidUser, isLoading: isPaidUserLoading } = useIsPaidUser();
const { data: limits, isLoading: isLimitLoading } = useQuery(
aiLimitOptions(),
queryClient,
);
const isLimitDataLoading = isPaidUserLoading || isLimitLoading;
useEffect(() => {
if (isLimitDataLoading) {
return;
}
if (!isPaidUser && limits && limits?.quiz?.used >= limits?.quiz?.limit) {
setError('You have reached the limit for this format');
setIsLoading(false);
setShowUpgradeModal(true);
return;
}
const params = getUrlParams();
const paramsTerm = params?.term;
const paramsFormat = params?.format;
@@ -48,7 +72,7 @@ export function GenerateAIQuiz(props: GenerateAIQuizProps) {
src: paramsSrc,
questionAndAnswers,
});
}, []);
}, [isLimitDataLoading, isPaidUser]);
const handleGenerateQuiz = async (options: {
term: string;
@@ -102,24 +126,43 @@ export function GenerateAIQuiz(props: GenerateAIQuizProps) {
});
};
const upgradeModal = showUpgradeModal ? (
<UpgradeAccountModal
onClose={() => {
window.location.href = '/ai/quiz';
}}
/>
) : null;
if (error) {
return (
<div className="absolute inset-0 z-20 flex h-full flex-col items-center justify-center bg-white">
<div className="flex flex-col items-center justify-center gap-2">
<AlertCircleIcon className="size-10 text-gray-500" />
<p className="text-center">{error}</p>
<>
{upgradeModal}
<div className="absolute inset-0 z-20 flex h-full flex-col items-center justify-center bg-white">
<div className="flex flex-col items-center justify-center gap-2">
<AlertCircleIcon className="size-10 text-gray-500" />
<p className="text-center">{error}</p>
</div>
</div>
</div>
</>
);
}
if (isLoading) {
return (
<div className="flex h-full w-full items-center justify-center">
<LoadingChip message="Please wait..." />
</div>
<>
{upgradeModal}
<div className="flex h-full w-full items-center justify-center">
<LoadingChip message="Please wait..." />
</div>
</>
);
}
return <AIQuizContent isStreaming={isStreaming} questions={questions} />;
return (
<>
{upgradeModal}
<AIQuizContent isStreaming={isStreaming} questions={questions} />
</>
);
}

View File

@@ -14,6 +14,9 @@ import {
type ListUserAiQuizzesQuery,
} from '../../queries/ai-quiz';
import { AIQuizCard } from './AIQuizCard';
import { aiLimitOptions } from '../../queries/ai-course';
import { useIsPaidUser } from '../../queries/billing';
import { AIUsageWarning } from '../AIUsageWarning/AIUsageWarning';
export function UserQuizzesList() {
const [isInitialLoading, setIsInitialLoading] = useState(true);
@@ -25,6 +28,14 @@ export function UserQuizzesList() {
query: '',
});
const { isPaidUser, isLoading: isPaidUserLoading } = useIsPaidUser();
const { data: limits, isLoading: isLimitLoading } = useQuery(
aiLimitOptions(),
queryClient,
);
const selectedLimit = limits?.quiz;
const { data: userAiQuizzes, isFetching: isUserAiQuizzesLoading } = useQuery(
listUserAiQuizzesOptions(pageState),
queryClient,
@@ -59,7 +70,11 @@ export function UserQuizzesList() {
}, [pageState]);
const isUserAuthenticated = isLoggedIn();
const isAnyLoading = isUserAiQuizzesLoading || isInitialLoading;
const isAnyLoading =
isUserAiQuizzesLoading ||
isInitialLoading ||
isPaidUserLoading ||
isLimitLoading;
return (
<>
@@ -89,11 +104,14 @@ export function UserQuizzesList() {
{!isAnyLoading && (
<>
<p className="mb-4 text-sm text-gray-500">
{isUserAuthenticated
? `You have generated ${userAiQuizzes?.totalCount} quizzes so far.`
: 'Sign up or login to generate your first quiz. Takes 2s to do so.'}
</p>
<AIUsageWarning
type="quiz"
totalCount={userAiQuizzes?.totalCount}
isPaidUser={isPaidUser}
usedCount={selectedLimit?.used}
limitCount={selectedLimit?.limit}
onUpgrade={() => setShowUpgradePopup(true)}
/>
{isUserAuthenticated && !isAnyLoading && quizzes.length > 0 && (
<div className="flex flex-col gap-2">

View File

@@ -14,7 +14,7 @@ import { AIRoadmapChat } from './AIRoadmapChat';
import { AlertCircleIcon } from 'lucide-react';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { aiLimitOptions } from '../../queries/ai-course';
import { billingDetailsOptions } from '../../queries/billing';
export type AIRoadmapChatActions = {
@@ -50,7 +50,7 @@ export function AIRoadmap(props: AIRoadmapProps) {
data: tokenUsage,
isLoading: isTokenUsageLoading,
refetch: refetchTokenUsage,
} = useQuery(getAiCourseLimitOptions(), queryClient);
} = useQuery(aiLimitOptions(), queryClient);
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
useQuery(billingDetailsOptions(), queryClient);

View File

@@ -24,7 +24,7 @@ import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import { flushSync } from 'react-dom';
import { markdownToHtml } from '../../lib/markdown';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { aiLimitOptions } from '../../queries/ai-course';
import { useQuery } from '@tanstack/react-query';
import { queryClient } from '../../stores/query-client';
import { billingDetailsOptions } from '../../queries/billing';
@@ -57,7 +57,7 @@ export function AIRoadmapChat(props: AIRoadmapChatProps) {
data: tokenUsage,
isLoading: isTokenUsageLoading,
refetch: refetchTokenUsage,
} = useQuery(getAiCourseLimitOptions(), queryClient);
} = useQuery(aiLimitOptions(), queryClient);
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
useQuery(billingDetailsOptions(), queryClient);

View File

@@ -7,6 +7,10 @@ import type { QuestionAnswerChatMessage } from '../ContentGenerator/QuestionAnsw
import { getQuestionAnswerChatMessages } from '../../lib/ai-questions';
import { aiRoadmapOptions, generateAIRoadmap } from '../../queries/ai-roadmap';
import { AIRoadmapContent } from './AIRoadmapContent';
import { useIsPaidUser } from '../../queries/billing';
import { useQuery } from '@tanstack/react-query';
import { aiLimitOptions } from '../../queries/ai-course';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
type GenerateAIRoadmapProps = {
onRoadmapSlugChange?: (roadmapSlug: string) => void;
@@ -18,12 +22,36 @@ export function GenerateAIRoadmap(props: GenerateAIRoadmapProps) {
const [isLoading, setIsLoading] = useState(true);
const [isStreaming, setIsStreaming] = useState(false);
const [error, setError] = useState('');
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
const [svgHtml, setSvgHtml] = useState('');
const [content, setContent] = useState('');
const svgRef = useRef<string | null>(null);
const { isPaidUser, isLoading: isPaidUserLoading } = useIsPaidUser();
const { data: limits, isLoading: isLimitLoading } = useQuery(
aiLimitOptions(),
queryClient,
);
const isLimitDataLoading = isPaidUserLoading || isLimitLoading;
useEffect(() => {
if (isLimitDataLoading) {
return;
}
if (
!isPaidUser &&
limits &&
limits?.roadmap?.used >= limits?.roadmap?.limit
) {
setError('You have reached the limit for this format');
setIsLoading(false);
setShowUpgradeModal(true);
return;
}
const params = getUrlParams();
const paramsTerm = params?.term;
const paramsSrc = params?.src || 'search';
@@ -42,7 +70,7 @@ export function GenerateAIRoadmap(props: GenerateAIRoadmapProps) {
src: paramsSrc,
questionAndAnswers,
});
}, []);
}, [isLimitDataLoading, isPaidUser]);
const handleGenerateDocument = async (options: {
term: string;
@@ -99,17 +127,39 @@ export function GenerateAIRoadmap(props: GenerateAIRoadmapProps) {
});
};
const upgradeModal = showUpgradeModal ? (
<UpgradeAccountModal
onClose={() => {
window.location.href = '/ai';
}}
/>
) : null;
if (error) {
return <div className="text-red-500">{error}</div>;
return (
<>
{upgradeModal}
<div className="text-red-500">{error}</div>
</>
);
}
if (isLoading) {
return (
<div className="flex h-full w-full items-center justify-center">
<LoadingChip message="Please wait..." />
</div>
<>
{upgradeModal}
<div className="flex h-full w-full items-center justify-center">
<LoadingChip message="Please wait..." />
</div>
</>
);
}
return <AIRoadmapContent isLoading={isLoading} svgHtml={svgHtml} />;
return (
<>
{upgradeModal}
<AIRoadmapContent isLoading={isLoading} svgHtml={svgHtml} />
</>
);
}

View File

@@ -14,6 +14,9 @@ import {
type ListUserAiRoadmapsQuery,
} from '../../queries/ai-roadmap';
import { AIRoadmapCard } from './AIRoadmapCard';
import { aiLimitOptions } from '../../queries/ai-course';
import { useIsPaidUser } from '../../queries/billing';
import { AIUsageWarning } from '../AIUsageWarning/AIUsageWarning';
export function UserRoadmapsList() {
const [isInitialLoading, setIsInitialLoading] = useState(true);
@@ -25,6 +28,14 @@ export function UserRoadmapsList() {
query: '',
});
const { isPaidUser, isLoading: isPaidUserLoading } = useIsPaidUser();
const { data: limits, isLoading: isLimitLoading } = useQuery(
aiLimitOptions(),
queryClient,
);
const selectedLimit = limits?.roadmap;
const { data: userAiRoadmaps, isFetching: isUserAiRoadmapsLoading } =
useQuery(listUserAiRoadmapsOptions(pageState), queryClient);
@@ -57,7 +68,11 @@ export function UserRoadmapsList() {
}, [pageState]);
const isUserAuthenticated = isLoggedIn();
const isAnyLoading = isUserAiRoadmapsLoading || isInitialLoading;
const isAnyLoading =
isUserAiRoadmapsLoading ||
isInitialLoading ||
isPaidUserLoading ||
isLimitLoading;
return (
<>
@@ -81,23 +96,30 @@ export function UserRoadmapsList() {
{isAnyLoading && (
<p className="mb-4 flex flex-row items-center gap-2 text-sm text-gray-500">
<Loader2 className="h-4 w-4 animate-spin" />
Loading your courses...
Loading your roadmaps...
</p>
)}
{!isAnyLoading && (
<>
<p className="mb-4 text-sm text-gray-500">
{isUserAuthenticated
? `You have generated ${userAiRoadmaps?.totalCount} roadmaps so far.`
: 'Sign up or login to generate your first roadmap. Takes 2s to do so.'}
</p>
<AIUsageWarning
type="roadmap"
totalCount={userAiRoadmaps?.totalCount}
isPaidUser={isPaidUser}
usedCount={selectedLimit?.used}
limitCount={selectedLimit?.limit}
onUpgrade={() => setShowUpgradePopup(true)}
/>
{isUserAuthenticated && !isAnyLoading && roadmaps.length > 0 && (
<div className="flex flex-col gap-2">
<div className="grid grid-cols-1 gap-2 md:grid-cols-2 xl:grid-cols-3">
{roadmaps.map((roadmap) => (
<AIRoadmapCard variant="column" key={roadmap._id} roadmap={roadmap} />
<AIRoadmapCard
variant="column"
key={roadmap._id}
roadmap={roadmap}
/>
))}
</div>

View File

@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { aiLimitOptions } from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
import { useIsPaidUser } from '../../queries/billing';
import { PlusIcon } from 'lucide-react';
@@ -14,7 +14,7 @@ type AITutorHeaderProps = {
export function AITutorHeader(props: AITutorHeaderProps) {
const { title, subtitle, onUpgradeClick, children } = props;
const { data: limits } = useQuery(getAiCourseLimitOptions(), queryClient);
const { data: limits } = useQuery(aiLimitOptions(), queryClient);
const { isPaidUser, isLoading: isPaidUserLoading } = useIsPaidUser();
const { used, limit } = limits ?? { used: 0, limit: 0 };
@@ -23,7 +23,7 @@ export function AITutorHeader(props: AITutorHeaderProps) {
<div className="mb-3 flex min-h-[35px] items-center justify-between max-sm:mb-1">
<div className="flex w-full flex-row items-center justify-between gap-2">
<div className="gap-2">
<h2 className="relative top-0 mb-1 sm:mb-3 flex-shrink-0 text-2xl sm:text-3xl font-semibold lg:top-1">
<h2 className="relative top-0 mb-1 flex-shrink-0 text-2xl font-semibold sm:mb-3 sm:text-3xl lg:top-1">
{title}
</h2>
{subtitle && <p className="mb-4 text-sm text-gray-500">{subtitle}</p>}
@@ -31,7 +31,7 @@ export function AITutorHeader(props: AITutorHeaderProps) {
<div className="flex flex-row items-center gap-2">
<a
href="/ai"
className="flex max-sm:hidden flex-row items-center gap-2 rounded-lg bg-black px-4 py-1.5 text-sm font-medium text-white"
className="flex flex-row items-center gap-2 rounded-lg bg-black px-4 py-1.5 text-sm font-medium text-white max-sm:hidden"
>
<PlusIcon className="h-4 w-4" />
New

View File

@@ -14,7 +14,7 @@ import { useIsPaidUser } from '../../queries/billing';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { AITutorLogo } from '../ReactIcons/AITutorLogo';
import { queryClient } from '../../stores/query-client';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { aiLimitOptions } from '../../queries/ai-course';
import { useQuery } from '@tanstack/react-query';
import { getPercentage } from '../../lib/number';
import { AILimitsPopup } from '../GenerateCourse/AILimitsPopup';
@@ -85,7 +85,7 @@ export function AITutorSidebar(props: AITutorSidebarProps) {
const { isPaidUser, isLoading: isPaidUserLoading } = useIsPaidUser();
const { data: limits, isLoading: isLimitsLoading } = useQuery(
getAiCourseLimitOptions(),
aiLimitOptions(),
queryClient,
);

View File

@@ -1,7 +1,7 @@
import { useQuery } from '@tanstack/react-query';
import { Zap } from 'lucide-react';
import { queryClient } from '../../stores/query-client';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { aiLimitOptions } from '../../queries/ai-course';
import { getPercentage } from '../../lib/number';
import { cn } from '../../lib/classname';
@@ -27,7 +27,7 @@ export function UpgradeSidebarCard(props: UpgradeSidebarCardProps) {
} = props;
const { data: limits, isLoading: isLimitsLoading } = useQuery(
getAiCourseLimitOptions(),
aiLimitOptions(),
queryClient,
);

View File

@@ -0,0 +1,51 @@
import { isLoggedIn } from '../../lib/jwt';
type AIUsageWarningProps = {
type: 'course' | 'guide' | 'roadmap' | 'quiz';
totalCount?: number;
isPaidUser?: boolean;
usedCount?: number;
limitCount?: number;
onUpgrade: () => void;
};
export function AIUsageWarning(props: AIUsageWarningProps) {
const { type, totalCount, isPaidUser, usedCount, limitCount, onUpgrade } =
props;
const isUserAuthenticated = isLoggedIn();
const typeLabels = {
course: 'courses',
guide: 'guides',
roadmap: 'roadmaps',
quiz: 'quizzes',
};
const typeLabel = typeLabels[type];
return (
<p className="mb-4 text-sm text-gray-500">
{isUserAuthenticated ? (
isPaidUser ? (
`You have generated ${totalCount} ${typeLabel} so far.`
) : (
<>
<span className="text-gray-500">You have used</span>{' '}
<span className="text-gray-500">
{usedCount} of {limitCount} {typeLabel}
</span>
<button
onClick={onUpgrade}
className="ml-2 text-blue-600 underline underline-offset-2 hover:text-blue-700"
>
Need more? Upgrade
</button>
</>
)
) : (
`Sign up or login to generate your first ${type}. Takes 2s to do so.`
)}
</p>
);
}

View File

@@ -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 <UpgradeAccountModal onClose={hideUpgradeModal} />;
}

View File

@@ -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<AllowedFormat>('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 && (
<UpgradeAccountModal onClose={() => setIsUpgradeModalOpen(false)} />
)}
{!isPaidUser && !isPaidUserLoading && isLoggedIn() && (
{showLimitWarning && (
<div className="absolute bottom-full left-1/2 -translate-x-1/2 -translate-y-8 text-gray-500 max-md:hidden">
You are on the free plan
{selectedLimit?.used} of {selectedLimit?.limit} {selectedFormat}s
<button
onClick={() => setIsUpgradeModalOpen(true)}
className="ml-2 rounded-xl bg-yellow-600 px-2 py-1 text-sm text-white hover:opacity-80"
>
Upgrade to Pro
Need more? Upgrade
</button>
</div>
)}
<h1 className="mb-0.5 text-center text-4xl font-semibold max-md:text-left max-md:text-xl lg:mb-3">
What can I help you learn?
</h1>
@@ -233,7 +255,7 @@ export function ContentGenerator() {
<button
type="submit"
className="flex w-full items-center justify-center gap-2 rounded-xl bg-black p-4 text-white focus:outline-none disabled:cursor-not-allowed disabled:opacity-80"
disabled={!canGenerate}
disabled={!canGenerate || isLimitLoading}
>
<SparklesIcon className="size-4" />
Generate

View File

@@ -26,7 +26,7 @@ import { lockBodyScroll } from '../../lib/dom';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import { slugify } from '../../lib/slugger';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { aiLimitOptions } from '../../queries/ai-course';
import { billingDetailsOptions } from '../../queries/billing';
import { chatHistoryOptions } from '../../queries/chat-history';
import { roadmapJSONOptions } from '../../queries/roadmap';
@@ -190,7 +190,7 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
const isAuthenticatedUser = isLoggedIn();
const { data: tokenUsage, isLoading: isTokenUsageLoading } = useQuery(
getAiCourseLimitOptions(),
aiLimitOptions(),
queryClient,
);
const isLimitExceeded =

View File

@@ -21,10 +21,7 @@ import {
} from '../../lib/markdown';
import { httpPatch } from '../../lib/query-http';
import { slugify } from '../../lib/slugger';
import {
getAiCourseLimitOptions,
getAiCourseOptions,
} from '../../queries/ai-course';
import { aiLimitOptions, getAiCourseOptions } from '../../queries/ai-course';
import { useIsPaidUser } from '../../queries/billing';
import { queryClient } from '../../stores/query-client';
import './AICourseLessonChat.css';
@@ -229,7 +226,7 @@ export function AICourseLesson(props: AICourseLessonProps) {
});
setLessonHtml(markdownHtml);
queryClient.invalidateQueries(getAiCourseLimitOptions());
queryClient.invalidateQueries(aiLimitOptions());
setIsGenerating(false);
},
});

View File

@@ -30,7 +30,7 @@ import {
markdownToHtml,
markdownToHtmlWithHighlighting,
} from '../../lib/markdown';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { aiLimitOptions } from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
import { billingDetailsOptions } from '../../queries/billing';
import { ResizablePanel } from './Resizeable';
@@ -93,7 +93,7 @@ export function AICourseLessonChat(props: AICourseLessonChatProps) {
const [streamedMessage, setStreamedMessage] = useState('');
const { data: tokenUsage, isLoading } = useQuery(
getAiCourseLimitOptions(),
aiLimitOptions(),
queryClient,
);
@@ -205,7 +205,7 @@ export function AICourseLessonChat(props: AICourseLessonChatProps) {
setCourseAIChatHistory(newMessages);
});
queryClient.invalidateQueries(getAiCourseLimitOptions());
queryClient.invalidateQueries(aiLimitOptions());
scrollToBottom();
},
});

View File

@@ -1,7 +1,7 @@
import { useQuery } from '@tanstack/react-query';
import { Gift, Info } from 'lucide-react';
import { getPercentage } from '../../lib/number';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { aiLimitOptions } from '../../queries/ai-course';
import { billingDetailsOptions } from '../../queries/billing';
import { queryClient } from '../../stores/query-client';
import { isLoggedIn } from '../../lib/jwt';
@@ -14,10 +14,7 @@ type AICourseLimitProps = {
export function AICourseLimit(props: AICourseLimitProps) {
const { onUpgrade, onShowLimits } = props;
const { data: limits, isLoading } = useQuery(
getAiCourseLimitOptions(),
queryClient,
);
const { data: limits, isLoading } = useQuery(aiLimitOptions(), queryClient);
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
useQuery(billingDetailsOptions(), queryClient);

View File

@@ -4,7 +4,7 @@ import { formatCommaNumber } from '../../lib/number';
import { billingDetailsOptions } from '../../queries/billing';
import { queryClient } from '../../stores/query-client';
import { useQuery } from '@tanstack/react-query';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { aiLimitOptions } from '../../queries/ai-course';
type AILimitsPopupProps = {
onClose: () => void;
@@ -14,10 +14,7 @@ type AILimitsPopupProps = {
export function AILimitsPopup(props: AILimitsPopupProps) {
const { onClose, onUpgrade } = props;
const { data: limits, isLoading } = useQuery(
getAiCourseLimitOptions(),
queryClient,
);
const { data: limits, isLoading } = useQuery(aiLimitOptions(), queryClient);
const { used, limit } = limits ?? { used: 0, limit: 0 };

View File

@@ -5,10 +5,12 @@ import type { AiCourse } from '../../lib/ai';
import { AICourseContent } from './AICourseContent';
import { generateCourse } from '../../helper/generate-ai-course';
import { useQuery } from '@tanstack/react-query';
import { getAiCourseOptions } from '../../queries/ai-course';
import { aiLimitOptions, getAiCourseOptions } from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
import type { QuestionAnswerChatMessage } from '../ContentGenerator/QuestionAnswerChat';
import { getQuestionAnswerChatMessages } from '../../lib/ai-questions';
import { useIsPaidUser } from '../../queries/billing';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
type GenerateAICourseProps = {};
@@ -18,8 +20,8 @@ export function GenerateAICourse(props: GenerateAICourseProps) {
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState('');
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
const [creatorId, setCreatorId] = useState('');
const [courseId, setCourseId] = useState('');
const [courseSlug, setCourseSlug] = useState('');
const [course, setCourse] = useState<AiCourse>({
title: '',
@@ -35,14 +37,34 @@ export function GenerateAICourse(props: GenerateAICourseProps) {
queryClient,
);
useEffect(() => {
if (aiCourse) {
setCourse(aiCourse);
}
}, [aiCourse]);
const { isPaidUser, isLoading: isPaidUserLoading } = useIsPaidUser();
const { data: limits, isLoading: isLimitLoading } = useQuery(
aiLimitOptions(),
queryClient,
);
useEffect(() => {
if (term) {
if (!aiCourse) {
return;
}
setCourse(aiCourse);
}, [aiCourse]);
const isLimitDataLoading = isPaidUserLoading || isLimitLoading;
useEffect(() => {
if (term || isLimitDataLoading) {
return;
}
if (
!isPaidUser &&
limits &&
limits?.course?.used >= limits?.course?.limit
) {
setError('You have reached the limit for this format');
setIsLoading(false);
setShowUpgradeModal(true);
return;
}
@@ -65,7 +87,7 @@ export function GenerateAICourse(props: GenerateAICourseProps) {
src: paramsSrc,
questionAndAnswers,
});
}, [term]);
}, [term, isLimitDataLoading]);
const handleGenerateCourse = async (options: {
term: string;
@@ -84,7 +106,6 @@ export function GenerateAICourse(props: GenerateAICourseProps) {
await generateCourse({
term,
slug: courseSlug,
onCourseIdChange: setCourseId,
onCourseSlugChange: setCourseSlug,
onCreatorIdChange: setCreatorId,
onCourseChange: setCourse,
@@ -98,19 +119,29 @@ export function GenerateAICourse(props: GenerateAICourseProps) {
};
return (
<AICourseContent
courseSlug={courseSlug}
creatorId={creatorId}
course={course}
isLoading={isLoading}
error={error}
onRegenerateOutline={(prompt) => {
handleGenerateCourse({
term,
isForce: true,
prompt,
});
}}
/>
<>
{showUpgradeModal && (
<UpgradeAccountModal
onClose={() => {
window.location.href = '/ai';
}}
/>
)}
<AICourseContent
courseSlug={courseSlug}
creatorId={creatorId}
course={course}
isLoading={isLoading}
error={error}
onRegenerateOutline={(prompt) => {
handleGenerateCourse({
term,
isForce: true,
prompt,
});
}}
/>
</>
);
}

View File

@@ -3,6 +3,7 @@ import { BookOpen, Loader2 } from 'lucide-react';
import { useEffect, useState } from 'react';
import { deleteUrlParam, getUrlParams, setUrlParams } from '../../lib/browser';
import {
aiLimitOptions,
listUserAiCoursesOptions,
type ListUserAiCoursesQuery,
} from '../../queries/ai-course';
@@ -14,6 +15,8 @@ import { AICourseCard } from './AICourseCard';
import { AICourseSearch } from './AICourseSearch';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import { useIsPaidUser } from '../../queries/billing';
import { AIUsageWarning } from '../AIUsageWarning/AIUsageWarning';
export function UserCoursesList() {
const [isInitialLoading, setIsInitialLoading] = useState(true);
@@ -25,6 +28,14 @@ export function UserCoursesList() {
query: '',
});
const { isPaidUser, isLoading: isPaidUserLoading } = useIsPaidUser();
const { data: limits, isLoading: isLimitLoading } = useQuery(
aiLimitOptions(),
queryClient,
);
const selectedLimit = limits?.course;
const { data: userAiCourses, isFetching: isUserAiCoursesLoading } = useQuery(
listUserAiCoursesOptions(pageState),
queryClient,
@@ -59,7 +70,11 @@ export function UserCoursesList() {
}, [pageState]);
const isUserAuthenticated = isLoggedIn();
const isAnyLoading = isUserAiCoursesLoading || isInitialLoading;
const isAnyLoading =
isUserAiCoursesLoading ||
isInitialLoading ||
isPaidUserLoading ||
isLimitLoading;
return (
<>
@@ -88,16 +103,23 @@ export function UserCoursesList() {
{!isAnyLoading && (
<>
<p className="mb-4 text-sm text-gray-500">
{isUserAuthenticated
? `You have generated ${userAiCourses?.totalCount} courses so far.`
: 'Sign up or login to generate your first course. Takes 2s to do so.'}
</p>
<AIUsageWarning
type="course"
totalCount={userAiCourses?.totalCount}
isPaidUser={isPaidUser}
usedCount={selectedLimit?.used}
limitCount={selectedLimit?.limit}
onUpgrade={() => setShowUpgradePopup(true)}
/>
{isUserAuthenticated && !isAnyLoading && courses.length > 0 && (
<div className="grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3 gap-2">
<div className="grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3">
{courses.map((course) => (
<AICourseCard variant="column" key={course._id} course={course} />
<AICourseCard
variant="column"
key={course._id}
course={course}
/>
))}
<Pagination

View File

@@ -16,7 +16,7 @@ import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { AIGuideChat } from './AIGuideChat';
import { AIGuideContent } from './AIGuideContent';
import { GenerateAIGuide } from './GenerateAIGuide';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { aiLimitOptions } from '../../queries/ai-course';
import { billingDetailsOptions } from '../../queries/billing';
import { showLoginPopup } from '../../lib/popup';
@@ -45,7 +45,7 @@ export function AIGuide(props: AIGuideProps) {
data: tokenUsage,
isLoading: isTokenUsageLoading,
refetch: refetchTokenUsage,
} = useQuery(getAiCourseLimitOptions(), queryClient);
} = useQuery(aiLimitOptions(), queryClient);
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
useQuery(billingDetailsOptions(), queryClient);

View File

@@ -22,7 +22,7 @@ import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import { flushSync } from 'react-dom';
import { markdownToHtml } from '../../lib/markdown';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { aiLimitOptions } from '../../queries/ai-course';
import { useQuery } from '@tanstack/react-query';
import { queryClient } from '../../stores/query-client';
import { billingDetailsOptions } from '../../queries/billing';
@@ -58,7 +58,7 @@ export function AIGuideChat(props: AIGuideChatProps) {
data: tokenUsage,
isLoading: isTokenUsageLoading,
refetch: refetchTokenUsage,
} = useQuery(getAiCourseLimitOptions(), queryClient);
} = useQuery(aiLimitOptions(), queryClient);
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
useQuery(billingDetailsOptions(), queryClient);

View File

@@ -9,6 +9,10 @@ import { getAiGuideOptions } from '../../queries/ai-guide';
import { LoadingChip } from '../LoadingChip';
import type { QuestionAnswerChatMessage } from '../ContentGenerator/QuestionAnswerChat';
import { getQuestionAnswerChatMessages } from '../../lib/ai-questions';
import { useIsPaidUser } from '../../queries/billing';
import { useQuery } from '@tanstack/react-query';
import { aiLimitOptions } from '../../queries/ai-course';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
type GenerateAIGuideProps = {
onGuideSlugChange?: (guideSlug: string) => void;
@@ -20,12 +24,32 @@ export function GenerateAIGuide(props: GenerateAIGuideProps) {
const [isLoading, setIsLoading] = useState(true);
const [isStreaming, setIsStreaming] = useState(false);
const [error, setError] = useState('');
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
const [content, setContent] = useState('');
const [html, setHtml] = useState('');
const htmlRef = useRef<string>('');
const { isPaidUser, isLoading: isPaidUserLoading } = useIsPaidUser();
const { data: limits, isLoading: isLimitLoading } = useQuery(
aiLimitOptions(),
queryClient,
);
const isLimitDataLoading = isPaidUserLoading || isLimitLoading;
useEffect(() => {
if (isLimitDataLoading) {
return;
}
if (!isPaidUser && limits && limits?.guide?.used >= limits?.guide?.limit) {
setError('You have reached the limit for this format');
setIsLoading(false);
setShowUpgradeModal(true);
return;
}
const params = getUrlParams();
const paramsTerm = params?.term;
const paramsSrc = params?.src || 'search';
@@ -44,7 +68,7 @@ export function GenerateAIGuide(props: GenerateAIGuideProps) {
src: paramsSrc,
questionAndAnswers,
});
}, []);
}, [isLimitDataLoading, isPaidUser]);
const handleGenerateDocument = async (options: {
term: string;
@@ -109,17 +133,38 @@ export function GenerateAIGuide(props: GenerateAIGuideProps) {
});
};
const upgradeModal = showUpgradeModal ? (
<UpgradeAccountModal
onClose={() => {
window.location.href = '/ai';
}}
/>
) : null;
if (error) {
return <div className="text-red-500">{error}</div>;
return (
<>
{upgradeModal}
<div className="text-red-500">{error}</div>
</>
);
}
if (isLoading) {
return (
<div className="flex h-full w-full items-center justify-center">
<LoadingChip message="Please wait..." />
</div>
<>
{upgradeModal}
<div className="flex h-full w-full items-center justify-center">
<LoadingChip message="Please wait..." />
</div>
</>
);
}
return <AIGuideContent html={html} />;
return (
<>
{upgradeModal}
<AIGuideContent html={html} />
</>
);
}

View File

@@ -14,6 +14,9 @@ import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { Pagination } from '../Pagination/Pagination';
import { AICourseSearch } from '../GenerateCourse/AICourseSearch';
import { AIGuideCard } from '../AIGuide/AIGuideCard';
import { useIsPaidUser } from '../../queries/billing';
import { aiLimitOptions } from '../../queries/ai-course';
import { AIUsageWarning } from '../AIUsageWarning/AIUsageWarning';
export function UserGuidesList() {
const [isInitialLoading, setIsInitialLoading] = useState(true);
@@ -25,6 +28,14 @@ export function UserGuidesList() {
query: '',
});
const { isPaidUser, isLoading: isPaidUserLoading } = useIsPaidUser();
const { data: limits, isLoading: isLimitLoading } = useQuery(
aiLimitOptions(),
queryClient,
);
const selectedLimit = limits?.guide;
const { data: userAiGuides, isFetching: isUserAiGuidesLoading } = useQuery(
listUserAIGuidesOptions(pageState),
queryClient,
@@ -59,7 +70,11 @@ export function UserGuidesList() {
}, [pageState]);
const isUserAuthenticated = isLoggedIn();
const isAnyLoading = isUserAiGuidesLoading || isInitialLoading;
const isAnyLoading =
isUserAiGuidesLoading ||
isInitialLoading ||
isPaidUserLoading ||
isLimitLoading;
return (
<>
@@ -89,11 +104,14 @@ export function UserGuidesList() {
{!isAnyLoading && (
<>
<p className="mb-4 text-sm text-gray-500">
{isUserAuthenticated
? `You have generated ${userAiGuides?.totalCount} guides so far.`
: 'Sign up or login to generate your first guide. Takes 2s to do so.'}
</p>
<AIUsageWarning
type="guide"
totalCount={userAiGuides?.totalCount}
isPaidUser={isPaidUser}
usedCount={selectedLimit?.used}
limitCount={selectedLimit?.limit}
onUpgrade={() => setShowUpgradePopup(true)}
/>
{isUserAuthenticated && !isAnyLoading && guides.length > 0 && (
<div className="flex flex-col gap-2">

View File

@@ -38,7 +38,7 @@ import {
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import { slugify } from '../../lib/slugger';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { aiLimitOptions } from '../../queries/ai-course';
import { billingDetailsOptions } from '../../queries/billing';
import { chatHistoryOptions } from '../../queries/chat-history';
import { userResourceProgressOptions } from '../../queries/resource-progress';
@@ -105,7 +105,7 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
);
const { data: tokenUsage, isLoading: isTokenUsageLoading } = useQuery(
getAiCourseLimitOptions(),
aiLimitOptions(),
queryClient,
);

View File

@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { aiLimitOptions } from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
import { billingDetailsOptions } from '../../queries/billing';
import { isLoggedIn } from '../../lib/jwt';
@@ -91,7 +91,7 @@ export function RoadmapAIChatHeader(props: RoadmapAIChatHeaderProps) {
} = props;
const [showAILimitsPopup, setShowAILimitsPopup] = useState(false);
const { data: tokenUsage } = useQuery(getAiCourseLimitOptions(), queryClient);
const { data: tokenUsage } = useQuery(aiLimitOptions(), queryClient);
const { data: userBillingDetails } = useQuery(
billingDetailsOptions(),

View File

@@ -20,7 +20,7 @@ import { markdownToHtmlWithHighlighting } from '../../lib/markdown';
import { getPercentage } from '../../lib/number';
import { showLoginPopup } from '../../lib/popup';
import type { ResourceType } from '../../lib/resource-progress';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { aiLimitOptions } from '../../queries/ai-course';
import { billingDetailsOptions } from '../../queries/billing';
import { roadmapTreeMappingOptions } from '../../queries/roadmap-tree';
import { queryClient } from '../../stores/query-client';
@@ -76,7 +76,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
const [streamedMessage, setStreamedMessage] = useState('');
const [showAILimitsPopup, setShowAILimitsPopup] = useState(false);
const { data: tokenUsage, isLoading } = useQuery(
getAiCourseLimitOptions(),
aiLimitOptions(),
queryClient,
);
@@ -170,7 +170,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
window.location.reload();
}
queryClient.invalidateQueries(getAiCourseLimitOptions());
queryClient.invalidateQueries(aiLimitOptions());
return;
}
@@ -206,7 +206,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
setAiChatHistory(newMessages);
});
queryClient.invalidateQueries(getAiCourseLimitOptions());
queryClient.invalidateQueries(aiLimitOptions());
scrollToBottom();
},
});

View File

@@ -4,7 +4,7 @@ import {
type AiCourse,
} from '../lib/ai';
import { queryClient } from '../stores/query-client';
import { getAiCourseLimitOptions } from '../queries/ai-course';
import { aiLimitOptions } from '../queries/ai-course';
import type { QuestionAnswerChatMessage } from '../components/ContentGenerator/QuestionAnswerChat';
type GenerateCourseOptions = {
@@ -157,7 +157,7 @@ export async function generateCourse(options: GenerateCourseOptions) {
.replace(CREATOR_ID_REGEX, '');
onLoadingChange?.(false);
queryClient.invalidateQueries(getAiCourseLimitOptions());
queryClient.invalidateQueries(aiLimitOptions());
},
});
} catch (error: any) {

View File

@@ -1,5 +1,5 @@
import { queryClient } from '../stores/query-client';
import { getAiCourseLimitOptions } from '../queries/ai-course';
import { aiLimitOptions } from '../queries/ai-course';
import { readChatStream } from '../lib/chat';
import { markdownToHtmlWithHighlighting } from '../lib/markdown';
import type { QuestionAnswerChatMessage } from '../components/ContentGenerator/QuestionAnswerChat';
@@ -114,7 +114,7 @@ export async function generateGuide(options: GenerateGuideOptions) {
onMessageEnd: async (message) => {
onGuideChange?.(message);
onHtmlChange?.(await markdownToHtmlWithHighlighting(message));
queryClient.invalidateQueries(getAiCourseLimitOptions());
queryClient.invalidateQueries(aiLimitOptions());
onStreamingChange?.(false);
},
onDetails: async (details) => {

View File

@@ -4,7 +4,7 @@ import { flushSync } from 'react-dom';
import { removeAuthToken } from '../lib/jwt';
import { readStream } from '../lib/ai';
import { useToast } from './use-toast';
import { getAiCourseLimitOptions } from '../queries/ai-course';
import { aiLimitOptions } from '../queries/ai-course';
import { queryClient } from '../stores/query-client';
import {
renderMessage,
@@ -184,7 +184,7 @@ export function useRoadmapAIChat(options: Options) {
removeAuthToken();
window.location.reload();
}
queryClient.invalidateQueries(getAiCourseLimitOptions());
queryClient.invalidateQueries(aiLimitOptions());
return;
}
@@ -225,7 +225,7 @@ export function useRoadmapAIChat(options: Options) {
setIsStreamingMessage(false);
setAiChatHistory(newMessages);
});
queryClient.invalidateQueries(getAiCourseLimitOptions());
queryClient.invalidateQueries(aiLimitOptions());
queryClient.invalidateQueries({
predicate: (query) => {
return (

View File

@@ -22,6 +22,7 @@ import type { ResourceType } from '../lib/resource-progress';
import Bluconic from '../components/Analytics/Bluconic.astro';
import OneTrust from '../components/Analytics/OneTrust.astro';
import { cn } from '../lib/classname';
import { GlobalUpgradeModal } from '../components/Billing/GlobalUpgradeModal';
export interface Props {
title: string;
@@ -250,6 +251,7 @@ const gaPageIdentifier = Astro.url.pathname
<Toaster client:only='react' />
<CommandMenu client:idle />
<PageProgress initialMessage={initialLoadingMessage} client:idle />
<GlobalUpgradeModal client:only='react' />
<GoogleAdSlot />
<slot name='after-footer' />

View File

@@ -54,15 +54,33 @@ export function getAiCourseOptions(params: GetAICourseParams) {
export type GetAICourseLimitResponse = {
used: number;
limit: number;
course: {
used: number;
limit: number;
};
guide: {
used: number;
limit: number;
};
roadmap: {
used: number;
limit: number;
};
quiz: {
used: number;
limit: number;
};
};
export function getAiCourseLimitOptions() {
export function aiLimitOptions() {
return queryOptions({
queryKey: ['ai-course-limit'],
queryFn: () => {
return httpGet<GetAICourseLimitResponse>(`/v1-get-ai-course-limit`);
},
enabled: !!isLoggedIn(),
retryOnMount: false,
refetchOnMount: false,
});
}

View File

@@ -2,7 +2,7 @@ import { nanoid } from 'nanoid';
import type { QuestionAnswerChatMessage } from '../components/ContentGenerator/QuestionAnswerChat';
import { readChatStream } from '../lib/chat';
import { queryClient } from '../stores/query-client';
import { getAiCourseLimitOptions } from './ai-course';
import { aiLimitOptions } from './ai-course';
import { queryOptions } from '@tanstack/react-query';
import { httpGet } from '../lib/query-http';
import { isLoggedIn } from '../lib/jwt';
@@ -113,7 +113,7 @@ export async function generateAIQuiz(options: GenerateAIQuizOptions) {
onQuestionsChange?.(questions);
},
onMessageEnd: async (result) => {
queryClient.invalidateQueries(getAiCourseLimitOptions());
queryClient.invalidateQueries(aiLimitOptions());
onStreamingChange?.(false);
},
onDetails: async (details) => {

View File

@@ -3,7 +3,7 @@ import { httpGet } from '../lib/query-http';
import { generateAICourseRoadmapStructure } from '../lib/ai';
import { generateAIRoadmapFromText, renderFlowJSON } from '@roadmapsh/editor';
import { queryClient } from '../stores/query-client';
import { getAiCourseLimitOptions } from '../queries/ai-course';
import { aiLimitOptions } from '../queries/ai-course';
import { readChatStream } from '../lib/chat';
import type { QuestionAnswerChatMessage } from '../components/ContentGenerator/QuestionAnswerChat';
import { isLoggedIn } from '../lib/jwt';
@@ -157,7 +157,7 @@ export async function generateAIRoadmap(options: GenerateAIRoadmapOptions) {
onRoadmapSvgChange?.(svg);
},
onMessageEnd: async () => {
queryClient.invalidateQueries(getAiCourseLimitOptions());
queryClient.invalidateQueries(aiLimitOptions());
onStreamingChange?.(false);
},
onDetails: async (details) => {

View File

@@ -0,0 +1,11 @@
import { atom } from 'nanostores';
export const isUpgradeModalOpen = atom(false);
export function showUpgradeModal() {
isUpgradeModalOpen.set(true);
}
export function hideUpgradeModal() {
isUpgradeModalOpen.set(false);
}