diff --git a/.astro/types.d.ts b/.astro/types.d.ts index 03d7cc43f..f964fe0cf 100644 --- a/.astro/types.d.ts +++ b/.astro/types.d.ts @@ -1,2 +1 @@ /// -/// \ No newline at end of file diff --git a/src/components/AIRoadmap/AIRoadmap.tsx b/src/components/AIRoadmap/AIRoadmap.tsx index a252a8f29..3e3e3e496 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,9 @@ export function AIRoadmap(props: AIRoadmapProps) { const toast = useToast(); const [showUpgradeModal, setShowUpgradeModal] = useState(false); const [isRegenerating, setIsRegenerating] = useState(false); + const [regeneratedSvgHtml, setRegeneratedSvgHtml] = useState( + null, + ); // only fetch the guide if the guideSlug is provided // otherwise we are still generating the guide @@ -34,6 +37,7 @@ export function AIRoadmap(props: AIRoadmapProps) { const handleRegenerate = async (prompt?: string) => { flushSync(() => { setIsRegenerating(true); + setRegeneratedSvgHtml(null); }); queryClient.cancelQueries(aiRoadmapOptions(roadmapSlug)); @@ -45,9 +49,28 @@ export function AIRoadmap(props: AIRoadmapProps) { return { ...old, data: '', - svg: null, + svgHtml: '', }; }); + + setRegeneratedSvgHtml(''); + 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 +85,10 @@ export function AIRoadmap(props: AIRoadmapProps) {
{roadmapSlug && ( )} {!roadmapSlug && ( diff --git a/src/components/AIRoadmap/AIRoadmapContent.tsx b/src/components/AIRoadmap/AIRoadmapContent.tsx index 41f37b18e..6d98447f2 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 (
- {isLoading && ( + {isLoading && !svgHtml && (
)} + + {onRegenerate && !isLoading && roadmapSlug && ( +
+ +
+ )}
); } diff --git a/src/components/AIRoadmap/AIRoadmapRegenerate.tsx b/src/components/AIRoadmap/AIRoadmapRegenerate.tsx new file mode 100644 index 000000000..f1eeeb211 --- /dev/null +++ b/src/components/AIRoadmap/AIRoadmapRegenerate.tsx @@ -0,0 +1,250 @@ +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'; +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'; +import { generateAIRoadmapFromText } from '@roadmapsh/editor'; +import { useToast } from '../../hooks/use-toast'; + +type AIRoadmapRegenerateProps = { + onRegenerate: (prompt?: string) => void; + roadmapSlug: string; +}; + +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); + 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, + ); + + 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 && ( + { + 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 && ( +
+ { + setIsDropdownVisible(false); + setShowUpdatePreferencesModal(true); + }} + icon={SettingsIcon} + label="Update Preferences" + /> + { + setIsDropdownVisible(false); + onRegenerate(); + }} + icon={RefreshCcw} + label="Regenerate" + /> + { + setIsDropdownVisible(false); + setShowPromptModal(true); + }} + 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 ( + + ); +} diff --git a/src/components/GenerateGuide/UpdatePreferences.tsx b/src/components/GenerateGuide/UpdatePreferences.tsx index 5dc39ce25..8ec1626af 100644 --- a/src/components/GenerateGuide/UpdatePreferences.tsx +++ b/src/components/GenerateGuide/UpdatePreferences.tsx @@ -48,6 +48,13 @@ export function UpdatePreferences(props: UpdatePreferencesProps) { console.log(questionAnswerChatMessages); console.log(defaultQuestionAndAnswers); + const userAnswers = questionAnswerChatMessages.filter( + (message) => message.role === 'user', + ); + + const hasAnsweredAllQuestions = + userAnswers.length === defaultQuestions?.length; + return ( - {hasChangedQuestionAndAnswers && ( + {hasChangedQuestionAndAnswers && hasAnsweredAllQuestions && (