diff --git a/src/components/AIRoadmap/AIRoadmap.tsx b/src/components/AIRoadmap/AIRoadmap.tsx index 615287808..5c11b3eba 100644 --- a/src/components/AIRoadmap/AIRoadmap.tsx +++ b/src/components/AIRoadmap/AIRoadmap.tsx @@ -12,6 +12,10 @@ import { GenerateAIRoadmap } from './GenerateAIRoadmap'; import { AIRoadmapContent, type RoadmapNodeDetails } from './AIRoadmapContent'; 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 { billingDetailsOptions } from '../../queries/billing'; export type AIRoadmapChatActions = { handleNodeClick: (node: RoadmapNodeDetails) => void; @@ -42,7 +46,29 @@ export function AIRoadmap(props: AIRoadmapProps) { error: aiRoadmapError, } = useQuery(aiRoadmapOptions(roadmapSlug), queryClient); + const { + data: tokenUsage, + isLoading: isTokenUsageLoading, + refetch: refetchTokenUsage, + } = useQuery(getAiCourseLimitOptions(), queryClient); + + const { data: userBillingDetails, isLoading: isBillingDetailsLoading } = + useQuery(billingDetailsOptions(), queryClient); + + const isLimitExceeded = (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0); + const isPaidUser = userBillingDetails?.status === 'active'; + const handleRegenerate = async (prompt?: string) => { + if (!isLoggedIn()) { + showLoginPopup(); + return; + } + + if (!isPaidUser && isLimitExceeded) { + setShowUpgradeModal(true); + return; + } + flushSync(() => { setIsRegenerating(true); setRegeneratedSvgHtml(null); @@ -76,12 +102,17 @@ export function AIRoadmap(props: AIRoadmapProps) { }, onFinish: () => { setIsRegenerating(false); + refetchTokenUsage(); queryClient.invalidateQueries(aiRoadmapOptions(roadmapSlug)); }, }); }; - const isLoading = isLoadingBySlug || isRegenerating; + const isLoading = + isLoadingBySlug || + isRegenerating || + isTokenUsageLoading || + isBillingDetailsLoading; const handleNodeClick = useCallback( (node: RoadmapNodeDetails) => { diff --git a/src/components/AIRoadmap/AIRoadmapChat.tsx b/src/components/AIRoadmap/AIRoadmapChat.tsx index ca36feb2e..b865471b0 100644 --- a/src/components/AIRoadmap/AIRoadmapChat.tsx +++ b/src/components/AIRoadmap/AIRoadmapChat.tsx @@ -124,6 +124,10 @@ export function AIRoadmapChat(props: AIRoadmapChatProps) { }); sendMessages(newMessages); setInputValue(''); + + setTimeout(() => { + scrollToBottom('smooth'); + }, 0); }, [inputValue, isStreamingMessage, messages, sendMessages, setMessages], ); @@ -174,11 +178,7 @@ export function AIRoadmapChat(props: AIRoadmapChatProps) { useImperativeHandle(aiChatActionsRef, () => ({ handleNodeClick: (node: RoadmapNodeDetails) => { - flushSync(() => { - setInputValue(`Explain what is ${node.nodeTitle} topic in detail.`); - }); - - inputRef.current?.focus(); + handleSubmitInput(`Explain what is "${node.nodeTitle}" topic in detail.`); }, })); @@ -304,6 +304,24 @@ export function AIRoadmapChat(props: AIRoadmapChatProps) { )} + {!isLoggedIn() && ( +
+ +

Please login to continue

+ +
+ )} + void; @@ -34,6 +37,7 @@ export function AIRoadmapRegenerate(props: AIRoadmapRegenerateProps) { const [showPromptModal, setShowPromptModal] = useState(false); const [showUpdatePreferencesModal, setShowUpdatePreferencesModal] = useState(false); + const currentUser = useAuth(); const ref = useRef(null); @@ -132,8 +136,11 @@ export function AIRoadmapRegenerate(props: AIRoadmapRegenerateProps) { queryClient, ); + const isCurrentUserCreator = currentUser?.id === aiRoadmap?.userId; const showUpdatePreferences = - aiRoadmap?.questionAndAnswers && aiRoadmap.questionAndAnswers.length > 0; + aiRoadmap?.questionAndAnswers && + aiRoadmap.questionAndAnswers.length > 0 && + isCurrentUserCreator; return ( <> @@ -180,45 +187,78 @@ export function AIRoadmapRegenerate(props: AIRoadmapRegenerateProps) { {isDropdownVisible && (
- {showUpdatePreferences && ( - { - setIsDropdownVisible(false); - setShowUpdatePreferencesModal(true); - }} - icon={SettingsIcon} - label="Update Preferences" - /> + {isCurrentUserCreator && ( + <> + {showUpdatePreferences && ( + { + if (!isLoggedIn()) { + showLoginPopup(); + return; + } + + setIsDropdownVisible(false); + setShowUpdatePreferencesModal(true); + }} + icon={SettingsIcon} + label="Update Preferences" + /> + )} + + { + if (!isLoggedIn()) { + showLoginPopup(); + return; + } + + setIsDropdownVisible(false); + onRegenerate(); + }} + icon={RefreshCcw} + label="Regenerate" + /> + { + if (!isLoggedIn()) { + showLoginPopup(); + return; + } + + setIsDropdownVisible(false); + setShowPromptModal(true); + }} + icon={PenSquare} + label="Modify Prompt" + /> + +
+ )} { - setIsDropdownVisible(false); - onRegenerate(); - }} - icon={RefreshCcw} - label="Regenerate" - /> - { - setIsDropdownVisible(false); - setShowPromptModal(true); - }} - icon={PenSquare} - label="Modify Prompt" - /> + if (!isLoggedIn()) { + showLoginPopup(); + return; + } -
- - { + if (!isLoggedIn()) { + showLoginPopup(); + return; + } + + editAIRoadmap(); + }} icon={PenSquare} label="Edit in Editor" isLoading={isEditingAIRoadmap} diff --git a/src/components/GenerateCourse/GetAICourse.tsx b/src/components/GenerateCourse/GetAICourse.tsx index 072b80689..dba0f8183 100644 --- a/src/components/GenerateCourse/GetAICourse.tsx +++ b/src/components/GenerateCourse/GetAICourse.tsx @@ -59,7 +59,6 @@ export function GetAICourse(props: GetAICourseProps) { await generateCourse({ term: aiCourse.keyword, - difficulty: aiCourse.difficulty, slug: courseSlug, prompt, onCourseChange: (course, rawData) => { @@ -68,7 +67,6 @@ export function GetAICourse(props: GetAICourseProps) { { ...aiCourse, title: course.title, - difficulty: course.difficulty, modules: course.modules, }, ); @@ -89,7 +87,6 @@ export function GetAICourse(props: GetAICourseProps) { course={{ title: aiCourse?.title || '', modules: aiCourse?.modules || [], - difficulty: aiCourse?.difficulty || 'Easy', done: aiCourse?.done || [], }} isLoading={isLoading || isRegenerating} diff --git a/src/components/GenerateGuide/AIGuide.tsx b/src/components/GenerateGuide/AIGuide.tsx index d5a653fed..7947d94c9 100644 --- a/src/components/GenerateGuide/AIGuide.tsx +++ b/src/components/GenerateGuide/AIGuide.tsx @@ -1,5 +1,5 @@ import { useQuery } from '@tanstack/react-query'; -import { ExternalLink } from 'lucide-react'; +import { AlertCircleIcon, ExternalLink } from 'lucide-react'; import { useMemo, useState } from 'react'; import { flushSync } from 'react-dom'; import { generateGuide } from '../../helper/generate-ai-guide'; @@ -16,6 +16,9 @@ import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal'; import { AIGuideChat } from './AIGuideChat'; import { AIGuideContent } from './AIGuideContent'; import { GenerateAIGuide } from './GenerateAIGuide'; +import { getAiCourseLimitOptions } from '../../queries/ai-course'; +import { billingDetailsOptions } from '../../queries/billing'; +import { showLoginPopup } from '../../lib/popup'; type AIGuideProps = { guideSlug?: string; @@ -32,10 +35,23 @@ export function AIGuide(props: AIGuideProps) { // only fetch the guide if the guideSlug is provided // otherwise we are still generating the guide - const { data: aiGuide, isLoading: isLoadingBySlug } = useQuery( - getAiGuideOptions(guideSlug), - queryClient, - ); + const { + data: aiGuide, + isLoading: isLoadingBySlug, + error: aiGuideError, + } = useQuery(getAiGuideOptions(guideSlug), queryClient); + + const { + data: tokenUsage, + isLoading: isTokenUsageLoading, + refetch: refetchTokenUsage, + } = useQuery(getAiCourseLimitOptions(), queryClient); + + const { data: userBillingDetails, isLoading: isBillingDetailsLoading } = + useQuery(billingDetailsOptions(), queryClient); + + const isLimitExceeded = (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0); + const isPaidUser = userBillingDetails?.status === 'active'; const { data: aiGuideSuggestions, isLoading: isAiGuideSuggestionsLoading } = useQuery( @@ -57,6 +73,16 @@ export function AIGuide(props: AIGuideProps) { }, [aiGuideSuggestions]); const handleRegenerate = async (prompt?: string) => { + if (!isLoggedIn()) { + showLoginPopup(); + return; + } + + if (!isPaidUser && isLimitExceeded) { + setShowUpgradeModal(true); + return; + } + flushSync(() => { setIsRegenerating(true); setRegeneratedHtml(null); @@ -92,25 +118,44 @@ export function AIGuide(props: AIGuideProps) { }); }; + const isLoading = + isLoadingBySlug || + isRegenerating || + isTokenUsageLoading || + isBillingDetailsLoading; + return ( {showUpgradeModal && ( setShowUpgradeModal(false)} /> )} + {!isLoading && aiGuideError && ( +
+
+ +

+ {aiGuideError?.message || 'Something went wrong'} +

+
+
+ )} +
- {guideSlug && ( + {guideSlug && !aiGuideError && ( )} - {!guideSlug && } + {!guideSlug && !aiGuideError && ( + + )} {aiGuide && !isRegenerating && (
diff --git a/src/components/GenerateGuide/AIGuideChat.tsx b/src/components/GenerateGuide/AIGuideChat.tsx index 407ddc00d..a3c1b3cc5 100644 --- a/src/components/GenerateGuide/AIGuideChat.tsx +++ b/src/components/GenerateGuide/AIGuideChat.tsx @@ -60,11 +60,8 @@ export function AIGuideChat(props: AIGuideChatProps) { refetch: refetchTokenUsage, } = useQuery(getAiCourseLimitOptions(), queryClient); - const { - data: userBillingDetails, - isLoading: isBillingDetailsLoading, - refetch: refetchBillingDetails, - } = useQuery(billingDetailsOptions(), queryClient); + const { data: userBillingDetails, isLoading: isBillingDetailsLoading } = + useQuery(billingDetailsOptions(), queryClient); const isLimitExceeded = (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0); const isPaidUser = userBillingDetails?.status === 'active'; @@ -127,6 +124,10 @@ export function AIGuideChat(props: AIGuideChatProps) { }); sendMessages(newMessages); setInputValue(''); + + setTimeout(() => { + scrollToBottom('smooth'); + }, 0); }, [inputValue, isStreamingMessage, messages, sendMessages, setMessages], ); @@ -332,6 +333,24 @@ export function AIGuideChat(props: AIGuideChatProps) {
)} + {!isLoggedIn() && ( +
+ +

Please login to continue

+ +
+ )} + void; @@ -24,6 +27,7 @@ export function AIGuideRegenerate(props: AIGuideRegenerateProps) { const [showPromptModal, setShowPromptModal] = useState(false); const [showUpdatePreferencesModal, setShowUpdatePreferencesModal] = useState(false); + const currentUser = useAuth(); const ref = useRef(null); @@ -61,7 +65,9 @@ export function AIGuideRegenerate(props: AIGuideRegenerateProps) { ); const showUpdatePreferences = - aiGuide?.questionAndAnswers && aiGuide.questionAndAnswers.length > 0; + aiGuide?.questionAndAnswers && + aiGuide.questionAndAnswers.length > 0 && + currentUser?.id === aiGuide.userId; return ( <> @@ -127,6 +133,11 @@ export function AIGuideRegenerate(props: AIGuideRegenerateProps) {