1
0
mirror of https://github.com/kamranahmedse/developer-roadmap.git synced 2025-09-03 14:22:41 +02:00
This commit is contained in:
Arik Chakma
2025-06-25 21:20:42 +06:00
parent a64587b836
commit ef7397cf4a
7 changed files with 162 additions and 19 deletions

View File

@@ -12,6 +12,10 @@ import { GenerateAIRoadmap } from './GenerateAIRoadmap';
import { AIRoadmapContent, type RoadmapNodeDetails } from './AIRoadmapContent'; import { AIRoadmapContent, type RoadmapNodeDetails } from './AIRoadmapContent';
import { AIRoadmapChat } from './AIRoadmapChat'; import { AIRoadmapChat } from './AIRoadmapChat';
import { AlertCircleIcon } from 'lucide-react'; 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 = { export type AIRoadmapChatActions = {
handleNodeClick: (node: RoadmapNodeDetails) => void; handleNodeClick: (node: RoadmapNodeDetails) => void;
@@ -42,7 +46,29 @@ export function AIRoadmap(props: AIRoadmapProps) {
error: aiRoadmapError, error: aiRoadmapError,
} = useQuery(aiRoadmapOptions(roadmapSlug), queryClient); } = 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) => { const handleRegenerate = async (prompt?: string) => {
if (!isLoggedIn()) {
showLoginPopup();
return;
}
if (!isPaidUser && isLimitExceeded) {
setShowUpgradeModal(true);
return;
}
flushSync(() => { flushSync(() => {
setIsRegenerating(true); setIsRegenerating(true);
setRegeneratedSvgHtml(null); setRegeneratedSvgHtml(null);
@@ -76,12 +102,17 @@ export function AIRoadmap(props: AIRoadmapProps) {
}, },
onFinish: () => { onFinish: () => {
setIsRegenerating(false); setIsRegenerating(false);
refetchTokenUsage();
queryClient.invalidateQueries(aiRoadmapOptions(roadmapSlug)); queryClient.invalidateQueries(aiRoadmapOptions(roadmapSlug));
}, },
}); });
}; };
const isLoading = isLoadingBySlug || isRegenerating; const isLoading =
isLoadingBySlug ||
isRegenerating ||
isTokenUsageLoading ||
isBillingDetailsLoading;
const handleNodeClick = useCallback( const handleNodeClick = useCallback(
(node: RoadmapNodeDetails) => { (node: RoadmapNodeDetails) => {

View File

@@ -124,6 +124,10 @@ export function AIRoadmapChat(props: AIRoadmapChatProps) {
}); });
sendMessages(newMessages); sendMessages(newMessages);
setInputValue(''); setInputValue('');
setTimeout(() => {
scrollToBottom('smooth');
}, 0);
}, },
[inputValue, isStreamingMessage, messages, sendMessages, setMessages], [inputValue, isStreamingMessage, messages, sendMessages, setMessages],
); );
@@ -174,11 +178,7 @@ export function AIRoadmapChat(props: AIRoadmapChatProps) {
useImperativeHandle(aiChatActionsRef, () => ({ useImperativeHandle(aiChatActionsRef, () => ({
handleNodeClick: (node: RoadmapNodeDetails) => { handleNodeClick: (node: RoadmapNodeDetails) => {
flushSync(() => { handleSubmitInput(`Explain what is "${node.nodeTitle}" topic in detail.`);
setInputValue(`Explain what is ${node.nodeTitle} topic in detail.`);
});
inputRef.current?.focus();
}, },
})); }));
@@ -304,6 +304,24 @@ export function AIRoadmapChat(props: AIRoadmapChatProps) {
</div> </div>
)} )}
{!isLoggedIn() && (
<div className="absolute inset-0 z-10 flex items-center justify-center gap-2 bg-black text-white">
<LockIcon
className="size-4 cursor-not-allowed"
strokeWidth={2.5}
/>
<p className="cursor-not-allowed">Please login to continue</p>
<button
onClick={() => {
showLoginPopup();
}}
className="rounded-md bg-white px-2 py-1 text-xs font-medium text-black hover:bg-gray-300"
>
Login / Register
</button>
</div>
)}
<input <input
ref={inputRef} ref={inputRef}
type="text" type="text"

View File

@@ -19,6 +19,9 @@ import { aiRoadmapOptions } from '../../queries/ai-roadmap';
import { UpdatePreferences } from '../GenerateGuide/UpdatePreferences'; import { UpdatePreferences } from '../GenerateGuide/UpdatePreferences';
import { generateAIRoadmapFromText } from '@roadmapsh/editor'; import { generateAIRoadmapFromText } from '@roadmapsh/editor';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
import { showLoginPopup } from '../../lib/popup';
import { isLoggedIn } from '../../lib/jwt';
import { useAuth } from '../../hooks/use-auth';
type AIRoadmapRegenerateProps = { type AIRoadmapRegenerateProps = {
onRegenerate: (prompt?: string) => void; onRegenerate: (prompt?: string) => void;
@@ -34,6 +37,7 @@ export function AIRoadmapRegenerate(props: AIRoadmapRegenerateProps) {
const [showPromptModal, setShowPromptModal] = useState(false); const [showPromptModal, setShowPromptModal] = useState(false);
const [showUpdatePreferencesModal, setShowUpdatePreferencesModal] = const [showUpdatePreferencesModal, setShowUpdatePreferencesModal] =
useState(false); useState(false);
const currentUser = useAuth();
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
@@ -133,7 +137,9 @@ export function AIRoadmapRegenerate(props: AIRoadmapRegenerateProps) {
); );
const showUpdatePreferences = const showUpdatePreferences =
aiRoadmap?.questionAndAnswers && aiRoadmap.questionAndAnswers.length > 0; aiRoadmap?.questionAndAnswers &&
aiRoadmap.questionAndAnswers.length > 0 &&
currentUser?.id === aiRoadmap.userId;
return ( return (
<> <>
@@ -183,6 +189,11 @@ export function AIRoadmapRegenerate(props: AIRoadmapRegenerateProps) {
{showUpdatePreferences && ( {showUpdatePreferences && (
<ActionButton <ActionButton
onClick={() => { onClick={() => {
if (!isLoggedIn()) {
showLoginPopup();
return;
}
setIsDropdownVisible(false); setIsDropdownVisible(false);
setShowUpdatePreferencesModal(true); setShowUpdatePreferencesModal(true);
}} }}
@@ -193,6 +204,11 @@ export function AIRoadmapRegenerate(props: AIRoadmapRegenerateProps) {
<ActionButton <ActionButton
onClick={() => { onClick={() => {
if (!isLoggedIn()) {
showLoginPopup();
return;
}
setIsDropdownVisible(false); setIsDropdownVisible(false);
onRegenerate(); onRegenerate();
}} }}
@@ -201,6 +217,11 @@ export function AIRoadmapRegenerate(props: AIRoadmapRegenerateProps) {
/> />
<ActionButton <ActionButton
onClick={() => { onClick={() => {
if (!isLoggedIn()) {
showLoginPopup();
return;
}
setIsDropdownVisible(false); setIsDropdownVisible(false);
setShowPromptModal(true); setShowPromptModal(true);
}} }}
@@ -211,14 +232,28 @@ export function AIRoadmapRegenerate(props: AIRoadmapRegenerateProps) {
<hr className="my-1 border-gray-200" /> <hr className="my-1 border-gray-200" />
<ActionButton <ActionButton
onClick={saveAIRoadmap} onClick={() => {
if (!isLoggedIn()) {
showLoginPopup();
return;
}
saveAIRoadmap();
}}
icon={SaveIcon} icon={SaveIcon}
label="Start Learning" label="Start Learning"
isLoading={isSavingAIRoadmap} isLoading={isSavingAIRoadmap}
/> />
<ActionButton <ActionButton
onClick={editAIRoadmap} onClick={() => {
if (!isLoggedIn()) {
showLoginPopup();
return;
}
editAIRoadmap();
}}
icon={PenSquare} icon={PenSquare}
label="Edit in Editor" label="Edit in Editor"
isLoading={isEditingAIRoadmap} isLoading={isEditingAIRoadmap}

View File

@@ -59,7 +59,6 @@ export function GetAICourse(props: GetAICourseProps) {
await generateCourse({ await generateCourse({
term: aiCourse.keyword, term: aiCourse.keyword,
difficulty: aiCourse.difficulty,
slug: courseSlug, slug: courseSlug,
prompt, prompt,
onCourseChange: (course, rawData) => { onCourseChange: (course, rawData) => {
@@ -68,7 +67,6 @@ export function GetAICourse(props: GetAICourseProps) {
{ {
...aiCourse, ...aiCourse,
title: course.title, title: course.title,
difficulty: course.difficulty,
modules: course.modules, modules: course.modules,
}, },
); );
@@ -89,7 +87,6 @@ export function GetAICourse(props: GetAICourseProps) {
course={{ course={{
title: aiCourse?.title || '', title: aiCourse?.title || '',
modules: aiCourse?.modules || [], modules: aiCourse?.modules || [],
difficulty: aiCourse?.difficulty || 'Easy',
done: aiCourse?.done || [], done: aiCourse?.done || [],
}} }}
isLoading={isLoading || isRegenerating} isLoading={isLoading || isRegenerating}

View File

@@ -16,6 +16,9 @@ import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { AIGuideChat } from './AIGuideChat'; import { AIGuideChat } from './AIGuideChat';
import { AIGuideContent } from './AIGuideContent'; import { AIGuideContent } from './AIGuideContent';
import { GenerateAIGuide } from './GenerateAIGuide'; import { GenerateAIGuide } from './GenerateAIGuide';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { billingDetailsOptions } from '../../queries/billing';
import { showLoginPopup } from '../../lib/popup';
type AIGuideProps = { type AIGuideProps = {
guideSlug?: string; guideSlug?: string;
@@ -37,6 +40,18 @@ export function AIGuide(props: AIGuideProps) {
queryClient, 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 } = const { data: aiGuideSuggestions, isLoading: isAiGuideSuggestionsLoading } =
useQuery( useQuery(
{ {
@@ -57,6 +72,16 @@ export function AIGuide(props: AIGuideProps) {
}, [aiGuideSuggestions]); }, [aiGuideSuggestions]);
const handleRegenerate = async (prompt?: string) => { const handleRegenerate = async (prompt?: string) => {
if (!isLoggedIn()) {
showLoginPopup();
return;
}
if (!isPaidUser && isLimitExceeded) {
setShowUpgradeModal(true);
return;
}
flushSync(() => { flushSync(() => {
setIsRegenerating(true); setIsRegenerating(true);
setRegeneratedHtml(null); setRegeneratedHtml(null);
@@ -92,6 +117,12 @@ export function AIGuide(props: AIGuideProps) {
}); });
}; };
const isLoading =
isLoadingBySlug ||
isRegenerating ||
isTokenUsageLoading ||
isBillingDetailsLoading;
return ( return (
<AITutorLayout <AITutorLayout
wrapperClassName="flex-row p-0 lg:p-0 overflow-hidden bg-white" wrapperClassName="flex-row p-0 lg:p-0 overflow-hidden bg-white"
@@ -106,7 +137,7 @@ export function AIGuide(props: AIGuideProps) {
<AIGuideContent <AIGuideContent
html={regeneratedHtml || aiGuide?.html || ''} html={regeneratedHtml || aiGuide?.html || ''}
onRegenerate={handleRegenerate} onRegenerate={handleRegenerate}
isLoading={isLoadingBySlug || isRegenerating} isLoading={isLoading}
guideSlug={guideSlug} guideSlug={guideSlug}
/> />
)} )}

View File

@@ -60,11 +60,8 @@ export function AIGuideChat(props: AIGuideChatProps) {
refetch: refetchTokenUsage, refetch: refetchTokenUsage,
} = useQuery(getAiCourseLimitOptions(), queryClient); } = useQuery(getAiCourseLimitOptions(), queryClient);
const { const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
data: userBillingDetails, useQuery(billingDetailsOptions(), queryClient);
isLoading: isBillingDetailsLoading,
refetch: refetchBillingDetails,
} = useQuery(billingDetailsOptions(), queryClient);
const isLimitExceeded = (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0); const isLimitExceeded = (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0);
const isPaidUser = userBillingDetails?.status === 'active'; const isPaidUser = userBillingDetails?.status === 'active';
@@ -332,6 +329,24 @@ export function AIGuideChat(props: AIGuideChatProps) {
</div> </div>
)} )}
{!isLoggedIn() && (
<div className="absolute inset-0 z-10 flex items-center justify-center gap-2 bg-black text-white">
<LockIcon
className="size-4 cursor-not-allowed"
strokeWidth={2.5}
/>
<p className="cursor-not-allowed">Please login to continue</p>
<button
onClick={() => {
showLoginPopup();
}}
className="rounded-md bg-white px-2 py-1 text-xs font-medium text-black hover:bg-gray-300"
>
Login / Register
</button>
</div>
)}
<input <input
ref={inputRef} ref={inputRef}
type="text" type="text"

View File

@@ -10,6 +10,9 @@ import { useMutation, useQuery } from '@tanstack/react-query';
import { getAiGuideOptions } from '../../queries/ai-guide'; import { getAiGuideOptions } from '../../queries/ai-guide';
import { queryClient } from '../../stores/query-client'; import { queryClient } from '../../stores/query-client';
import { httpPost } from '../../lib/query-http'; import { httpPost } from '../../lib/query-http';
import { useAuth } from '../../hooks/use-auth';
import { showLoginPopup } from '../../lib/popup';
import { isLoggedIn } from '../../lib/jwt';
type AIGuideRegenerateProps = { type AIGuideRegenerateProps = {
onRegenerate: (prompt?: string) => void; onRegenerate: (prompt?: string) => void;
@@ -24,6 +27,7 @@ export function AIGuideRegenerate(props: AIGuideRegenerateProps) {
const [showPromptModal, setShowPromptModal] = useState(false); const [showPromptModal, setShowPromptModal] = useState(false);
const [showUpdatePreferencesModal, setShowUpdatePreferencesModal] = const [showUpdatePreferencesModal, setShowUpdatePreferencesModal] =
useState(false); useState(false);
const currentUser = useAuth();
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
@@ -61,7 +65,9 @@ export function AIGuideRegenerate(props: AIGuideRegenerateProps) {
); );
const showUpdatePreferences = const showUpdatePreferences =
aiGuide?.questionAndAnswers && aiGuide.questionAndAnswers.length > 0; aiGuide?.questionAndAnswers &&
aiGuide.questionAndAnswers.length > 0 &&
currentUser?.id === aiGuide.userId;
return ( return (
<> <>
@@ -127,6 +133,11 @@ export function AIGuideRegenerate(props: AIGuideRegenerateProps) {
<button <button
onClick={() => { onClick={() => {
if (!isLoggedIn()) {
showLoginPopup();
return;
}
setIsDropdownVisible(false); setIsDropdownVisible(false);
onRegenerate(); onRegenerate();
}} }}
@@ -141,6 +152,11 @@ export function AIGuideRegenerate(props: AIGuideRegenerateProps) {
</button> </button>
<button <button
onClick={() => { onClick={() => {
if (!isLoggedIn()) {
showLoginPopup();
return;
}
setIsDropdownVisible(false); setIsDropdownVisible(false);
setShowPromptModal(true); setShowPromptModal(true);
}} }}