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 && (
-
-
+
{
setIsDropdownVisible(false);
onRegenerate();
}}
- className="flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm text-gray-600 hover:bg-gray-100"
- >
-
- Regenerate
-
-
+ {
setIsDropdownVisible(false);
setShowPromptModal(true);
}}
- className="flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm text-gray-600 hover:bg-gray-100"
- >
-
- Modify Prompt
-
+ 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 (
+
+ );
+}