From ae681a58b87cc9f9382c3510b8c9a2697c06888b Mon Sep 17 00:00:00 2001 From: Arik Chakma Date: Tue, 24 Jun 2025 23:20:36 +0600 Subject: [PATCH 1/3] wip --- .../AIRoadmap/AIRoadmapRegenerate.tsx | 158 ++++++++++++++++++ .../GenerateGuide/UpdatePreferences.tsx | 2 +- src/queries/ai-roadmap.ts | 3 + 3 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 src/components/AIRoadmap/AIRoadmapRegenerate.tsx diff --git a/src/components/AIRoadmap/AIRoadmapRegenerate.tsx b/src/components/AIRoadmap/AIRoadmapRegenerate.tsx new file mode 100644 index 000000000..ac5d198ff --- /dev/null +++ b/src/components/AIRoadmap/AIRoadmapRegenerate.tsx @@ -0,0 +1,158 @@ +import { PenSquare, RefreshCcw, SettingsIcon } from 'lucide-react'; +import { useRef, useState } from 'react'; +import { useOutsideClick } from '../../hooks/use-outside-click'; +import { cn } from '../../lib/classname'; +import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal'; +import { ModifyCoursePrompt } from '../GenerateCourse/ModifyCoursePrompt'; +import type { QuestionAnswerChatMessage } from '../ContentGenerator/QuestionAnswerChat'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { queryClient } from '../../stores/query-client'; +import { httpPost } from '../../lib/query-http'; +import { aiRoadmapOptions } from '../../queries/ai-roadmap'; +import { UpdatePreferences } from '../GenerateGuide/UpdatePreferences'; + +type AIRoadmapRegenerateProps = { + onRegenerate: (prompt?: string) => void; + roadmapSlug: string; +}; + +export function AIRoadmapRegenerate(props: AIRoadmapRegenerateProps) { + const { onRegenerate, roadmapSlug } = props; + + const [isDropdownVisible, setIsDropdownVisible] = useState(false); + const [showUpgradeModal, setShowUpgradeModal] = useState(false); + const [showPromptModal, setShowPromptModal] = useState(false); + const [showUpdatePreferencesModal, setShowUpdatePreferencesModal] = + useState(false); + + const ref = useRef(null); + + useOutsideClick(ref, () => setIsDropdownVisible(false)); + + const { data: aiRoadmap } = useQuery( + aiRoadmapOptions(roadmapSlug), + queryClient, + ); + const { mutate: updatePreferences, isPending: isUpdating } = useMutation( + { + mutationFn: (questionAndAnswers: QuestionAnswerChatMessage[]) => { + return httpPost(`/v1-update-ai-roadmap-preferences/${roadmapSlug}`, { + questionAndAnswers, + }); + }, + onSuccess: (_, vars) => { + queryClient.setQueryData( + aiRoadmapOptions(roadmapSlug).queryKey, + (old) => { + if (!old) { + return old; + } + + return { + ...old, + questionAndAnswers: vars, + }; + }, + ); + + setShowUpdatePreferencesModal(false); + setIsDropdownVisible(false); + onRegenerate(); + }, + }, + queryClient, + ); + + return ( + <> + {showUpgradeModal && ( + { + setShowUpgradeModal(false); + }} + /> + )} + + {showPromptModal && ( + setShowPromptModal(false)} + onSubmit={(prompt) => { + setShowPromptModal(false); + onRegenerate(prompt); + }} + /> + )} + + {showUpdatePreferencesModal && ( + setShowUpdatePreferencesModal(false)} + questionAndAnswers={aiRoadmap?.questionAndAnswers} + term={aiRoadmap?.term || ''} + format="roadmap" + onUpdatePreferences={(questionAndAnswers) => { + updatePreferences(questionAndAnswers); + }} + isUpdating={isUpdating} + /> + )} + +
+ + {isDropdownVisible && ( +
+ + + +
+ )} +
+ + ); +} diff --git a/src/components/GenerateGuide/UpdatePreferences.tsx b/src/components/GenerateGuide/UpdatePreferences.tsx index 8869edcd7..c3f485a46 100644 --- a/src/components/GenerateGuide/UpdatePreferences.tsx +++ b/src/components/GenerateGuide/UpdatePreferences.tsx @@ -55,7 +55,7 @@ export function UpdatePreferences(props: UpdatePreferencesProps) {

Update Preferences

- Update the preferences for the AI to generate a guide. + Update the preferences for the AI to generate a {format}.

diff --git a/src/queries/ai-roadmap.ts b/src/queries/ai-roadmap.ts index 7ecac3f6b..ce4f7725c 100644 --- a/src/queries/ai-roadmap.ts +++ b/src/queries/ai-roadmap.ts @@ -14,6 +14,9 @@ export interface AIRoadmapDocument { viewCount: number; lastVisitedAt: Date; keyType?: 'system' | 'user'; + + questionAndAnswers?: QuestionAnswerChatMessage[]; + createdAt: Date; updatedAt: Date; } From b0c3b1505ce43575512db412944215912eab3594 Mon Sep 17 00:00:00 2001 From: Arik Chakma Date: Tue, 24 Jun 2025 23:48:59 +0600 Subject: [PATCH 2/3] wip --- src/components/AIRoadmap/AIRoadmap.tsx | 27 ++- src/components/AIRoadmap/AIRoadmapContent.tsx | 14 +- .../AIRoadmap/AIRoadmapRegenerate.tsx | 154 ++++++++++++++---- 3 files changed, 160 insertions(+), 35 deletions(-) diff --git a/src/components/AIRoadmap/AIRoadmap.tsx b/src/components/AIRoadmap/AIRoadmap.tsx index a252a8f29..abb78b093 100644 --- a/src/components/AIRoadmap/AIRoadmap.tsx +++ b/src/components/AIRoadmap/AIRoadmap.tsx @@ -7,7 +7,7 @@ import { useToast } from '../../hooks/use-toast'; import { queryClient } from '../../stores/query-client'; import { AITutorLayout } from '../AITutor/AITutorLayout'; import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal'; -import { aiRoadmapOptions } from '../../queries/ai-roadmap'; +import { aiRoadmapOptions, generateAIRoadmap } from '../../queries/ai-roadmap'; import { GenerateAIRoadmap } from './GenerateAIRoadmap'; import { AIRoadmapContent } from './AIRoadmapContent'; import { AIRoadmapChat } from './AIRoadmapChat'; @@ -23,6 +23,7 @@ export function AIRoadmap(props: AIRoadmapProps) { const toast = useToast(); const [showUpgradeModal, setShowUpgradeModal] = useState(false); const [isRegenerating, setIsRegenerating] = useState(false); + const [regeneratedSvgHtml, setRegeneratedSvgHtml] = useState(''); // only fetch the guide if the guideSlug is provided // otherwise we are still generating the guide @@ -45,9 +46,27 @@ export function AIRoadmap(props: AIRoadmapProps) { return { ...old, data: '', - svg: null, + svgHtml: '', }; }); + + await generateAIRoadmap({ + roadmapSlug: aiRoadmap?.slug || '', + term: aiRoadmap?.term || '', + prompt, + isForce: true, + onStreamingChange: setIsRegenerating, + onRoadmapSvgChange: (svg) => { + setRegeneratedSvgHtml(svg.outerHTML); + }, + onError: (error) => { + toast.error(error); + }, + onFinish: () => { + setIsRegenerating(false); + queryClient.invalidateQueries(aiRoadmapOptions(roadmapSlug)); + }, + }); }; return ( @@ -62,8 +81,10 @@ export function AIRoadmap(props: AIRoadmapProps) {
{roadmapSlug && ( )} {!roadmapSlug && ( diff --git a/src/components/AIRoadmap/AIRoadmapContent.tsx b/src/components/AIRoadmap/AIRoadmapContent.tsx index 41f37b18e..11619c7d5 100644 --- a/src/components/AIRoadmap/AIRoadmapContent.tsx +++ b/src/components/AIRoadmap/AIRoadmapContent.tsx @@ -1,13 +1,16 @@ import { cn } from '../../lib/classname'; +import { AIRoadmapRegenerate } from './AIRoadmapRegenerate'; import { LoadingChip } from '../LoadingChip'; type AIRoadmapContentProps = { isLoading?: boolean; svgHtml: string; + onRegenerate?: (prompt?: string) => void; + roadmapSlug?: string; }; export function AIRoadmapContent(props: AIRoadmapContentProps) { - const { isLoading, svgHtml } = props; + const { isLoading, svgHtml, onRegenerate, roadmapSlug } = props; return (
)} + + {onRegenerate && !isLoading && roadmapSlug && ( +
+ +
+ )}
); } diff --git a/src/components/AIRoadmap/AIRoadmapRegenerate.tsx b/src/components/AIRoadmap/AIRoadmapRegenerate.tsx index ac5d198ff..f1eeeb211 100644 --- a/src/components/AIRoadmap/AIRoadmapRegenerate.tsx +++ b/src/components/AIRoadmap/AIRoadmapRegenerate.tsx @@ -1,4 +1,11 @@ -import { PenSquare, RefreshCcw, SettingsIcon } from 'lucide-react'; +import { + Loader2Icon, + PenSquare, + RefreshCcw, + SaveIcon, + SettingsIcon, + type LucideIcon, +} from 'lucide-react'; import { useRef, useState } from 'react'; import { useOutsideClick } from '../../hooks/use-outside-click'; import { cn } from '../../lib/classname'; @@ -10,6 +17,8 @@ import { queryClient } from '../../stores/query-client'; import { httpPost } from '../../lib/query-http'; import { aiRoadmapOptions } from '../../queries/ai-roadmap'; import { UpdatePreferences } from '../GenerateGuide/UpdatePreferences'; +import { generateAIRoadmapFromText } from '@roadmapsh/editor'; +import { useToast } from '../../hooks/use-toast'; type AIRoadmapRegenerateProps = { onRegenerate: (prompt?: string) => void; @@ -19,6 +28,7 @@ type AIRoadmapRegenerateProps = { export function AIRoadmapRegenerate(props: AIRoadmapRegenerateProps) { const { onRegenerate, roadmapSlug } = props; + const toast = useToast(); const [isDropdownVisible, setIsDropdownVisible] = useState(false); const [showUpgradeModal, setShowUpgradeModal] = useState(false); const [showPromptModal, setShowPromptModal] = useState(false); @@ -63,6 +73,65 @@ export function AIRoadmapRegenerate(props: AIRoadmapRegenerateProps) { queryClient, ); + const handleSaveAIRoadmap = async () => { + const { nodes, edges } = generateAIRoadmapFromText(aiRoadmap?.data || ''); + return httpPost<{ + roadmapId: string; + roadmapSlug: string; + }>(`/v1-save-ai-roadmap/${aiRoadmap?._id}`, { + title: aiRoadmap?.term, + nodes: nodes.map((node) => ({ + ...node, + + // To reset the width and height of the node + // so that it can be calculated based on the content in the editor + width: undefined, + height: undefined, + style: { + ...node.style, + width: undefined, + height: undefined, + }, + measured: { + width: undefined, + height: undefined, + }, + })), + edges, + }); + }; + + const { mutate: saveAIRoadmap, isPending: isSavingAIRoadmap } = useMutation( + { + mutationFn: handleSaveAIRoadmap, + onSuccess: (data) => { + if (!data?.roadmapId) { + toast.error('Something went wrong'); + return; + } + window.location.href = `/r/${data?.roadmapSlug}`; + }, + }, + queryClient, + ); + + const { mutate: editAIRoadmap, isPending: isEditingAIRoadmap } = useMutation( + { + mutationFn: handleSaveAIRoadmap, + onSuccess: (data) => { + if (!data?.roadmapId) { + toast.error('Something went wrong'); + return; + } + window.open( + `${import.meta.env.PUBLIC_EDITOR_APP_URL}/${data?.roadmapId}`, + '_blank', + ); + }, + }, + queryClient, + ); + return ( <> {showUpgradeModal && ( @@ -108,51 +177,74 @@ export function AIRoadmapRegenerate(props: AIRoadmapRegenerateProps) { {isDropdownVisible && (
- - - + icon={PenSquare} + label="Modify Prompt" + /> + + + +
)} ); } + +type ActionButtonProps = { + onClick: () => void; + isLoading?: boolean; + icon: LucideIcon; + label: string; +}; + +function ActionButton(props: ActionButtonProps) { + const { onClick, isLoading, icon: Icon, label } = props; + + return ( + + ); +} From 83720b387ceda8df5e755480584d8e03283fd39e Mon Sep 17 00:00:00 2001 From: Arik Chakma Date: Wed, 25 Jun 2025 00:01:34 +0600 Subject: [PATCH 3/3] fix: ai roadmap regenerate --- src/components/AIRoadmap/AIRoadmap.tsx | 8 ++++++-- src/components/AIRoadmap/AIRoadmapContent.tsx | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/AIRoadmap/AIRoadmap.tsx b/src/components/AIRoadmap/AIRoadmap.tsx index abb78b093..3e3e3e496 100644 --- a/src/components/AIRoadmap/AIRoadmap.tsx +++ b/src/components/AIRoadmap/AIRoadmap.tsx @@ -23,7 +23,9 @@ export function AIRoadmap(props: AIRoadmapProps) { const toast = useToast(); const [showUpgradeModal, setShowUpgradeModal] = useState(false); const [isRegenerating, setIsRegenerating] = useState(false); - const [regeneratedSvgHtml, setRegeneratedSvgHtml] = useState(''); + const [regeneratedSvgHtml, setRegeneratedSvgHtml] = useState( + null, + ); // only fetch the guide if the guideSlug is provided // otherwise we are still generating the guide @@ -35,6 +37,7 @@ export function AIRoadmap(props: AIRoadmapProps) { const handleRegenerate = async (prompt?: string) => { flushSync(() => { setIsRegenerating(true); + setRegeneratedSvgHtml(null); }); queryClient.cancelQueries(aiRoadmapOptions(roadmapSlug)); @@ -50,6 +53,7 @@ export function AIRoadmap(props: AIRoadmapProps) { }; }); + setRegeneratedSvgHtml(''); await generateAIRoadmap({ roadmapSlug: aiRoadmap?.slug || '', term: aiRoadmap?.term || '', @@ -81,7 +85,7 @@ export function AIRoadmap(props: AIRoadmapProps) {
{roadmapSlug && ( - {isLoading && ( + {isLoading && !svgHtml && (