feat/ai-rdm-slug
Arik Chakma 7 months ago
parent 231013744a
commit 9166b56af7
  1. 41
      src/components/GenerateRoadmap/GenerateRoadmap.tsx
  2. 29
      src/components/GenerateRoadmap/RoadmapTopicDetail.tsx
  3. 3
      src/pages/ai/[aiRoadmapSlug]/index.astro

@ -50,6 +50,7 @@ export type GetAIRoadmapLimitResponse = {
}; };
const ROADMAP_ID_REGEX = new RegExp('@ROADMAPID:(\\w+)@'); const ROADMAP_ID_REGEX = new RegExp('@ROADMAPID:(\\w+)@');
const ROADMAP_SLUG_REGEX = new RegExp(/@ROADMAPSLUG:([\w-]+)@/);
export type RoadmapNodeDetails = { export type RoadmapNodeDetails = {
nodeId: string; nodeId: string;
@ -89,11 +90,11 @@ type GetAIRoadmapResponse = {
type GenerateRoadmapProps = { type GenerateRoadmapProps = {
roadmapId?: string; roadmapId?: string;
t?: string; slug?: string;
}; };
export function GenerateRoadmap(props: GenerateRoadmapProps) { export function GenerateRoadmap(props: GenerateRoadmapProps) {
const { roadmapId, t: term = '' } = props; const { roadmapId: defaultRoadmapId, slug: defaultRoadmapSlug } = props;
const roadmapContainerRef = useRef<HTMLDivElement>(null); const roadmapContainerRef = useRef<HTMLDivElement>(null);
@ -102,13 +103,19 @@ export function GenerateRoadmap(props: GenerateRoadmapProps) {
}; };
const toast = useToast(); const toast = useToast();
const [roadmapId, setRoadmapId] = useState<string | undefined>(
defaultRoadmapId,
);
const [roadmapSlug, setRoadmapSlug] = useState<string | undefined>(
defaultRoadmapSlug,
);
const [hasSubmitted, setHasSubmitted] = useState<boolean>(Boolean(roadmapId)); const [hasSubmitted, setHasSubmitted] = useState<boolean>(Boolean(roadmapId));
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isLoadingResults, setIsLoadingResults] = useState(false); const [isLoadingResults, setIsLoadingResults] = useState(false);
const [roadmapTerm, setRoadmapTerm] = useState(term); const [roadmapTerm, setRoadmapTerm] = useState('');
const [generatedRoadmapContent, setGeneratedRoadmapContent] = useState('');
const [currentRoadmap, setCurrentRoadmap] = const [currentRoadmap, setCurrentRoadmap] =
useState<GetAIRoadmapResponse | null>(null); useState<GetAIRoadmapResponse | null>(null);
const [generatedRoadmapContent, setGeneratedRoadmapContent] = useState('');
const [selectedNode, setSelectedNode] = useState<RoadmapNodeDetails | null>( const [selectedNode, setSelectedNode] = useState<RoadmapNodeDetails | null>(
null, null,
); );
@ -140,6 +147,8 @@ export function GenerateRoadmap(props: GenerateRoadmapProps) {
deleteUrlParam('id'); deleteUrlParam('id');
setCurrentRoadmap(null); setCurrentRoadmap(null);
const origin = window.location.origin;
window.history.pushState(null, '', `${origin}/ai`);
const response = await fetch( const response = await fetch(
`${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-roadmap`, `${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-roadmap`,
{ {
@ -175,13 +184,21 @@ export function GenerateRoadmap(props: GenerateRoadmapProps) {
await readAIRoadmapStream(reader, { await readAIRoadmapStream(reader, {
onStream: async (result) => { onStream: async (result) => {
if (result.includes('@ROADMAPID')) { if (result.includes('@ROADMAPID') || result.includes('@ROADMAPSLUG')) {
// @ROADMAPID: is a special token that we use to identify the roadmap // @ROADMAPID: is a special token that we use to identify the roadmap
// @ROADMAPID:1234@ is the format, we will remove the token and the id // @ROADMAPID:1234@ is the format, we will remove the token and the id
// and replace it with a empty string // and replace it with a empty string
const roadmapId = result.match(ROADMAP_ID_REGEX)?.[1] || ''; const roadmapId = result.match(ROADMAP_ID_REGEX)?.[1] || '';
setUrlParams({ id: roadmapId }); const roadmapSlug = result.match(ROADMAP_SLUG_REGEX)?.[1] || '';
result = result.replace(ROADMAP_ID_REGEX, '');
window.history.pushState(null, '', `${origin}/ai/${roadmapSlug}`);
result = result
.replace(ROADMAP_ID_REGEX, '')
.replace(ROADMAP_SLUG_REGEX, '');
setRoadmapId(roadmapId);
setRoadmapSlug(roadmapSlug);
const roadmapTitle = const roadmapTitle =
result.trim().split('\n')[0]?.replace('#', '')?.trim() || term; result.trim().split('\n')[0]?.replace('#', '')?.trim() || term;
setRoadmapTerm(roadmapTitle); setRoadmapTerm(roadmapTitle);
@ -196,7 +213,10 @@ export function GenerateRoadmap(props: GenerateRoadmapProps) {
await renderRoadmap(result); await renderRoadmap(result);
}, },
onStreamEnd: async (result) => { onStreamEnd: async (result) => {
result = result.replace(ROADMAP_ID_REGEX, ''); result = result
.replace(ROADMAP_ID_REGEX, '')
.replace(ROADMAP_SLUG_REGEX, '');
setGeneratedRoadmapContent(result); setGeneratedRoadmapContent(result);
loadAIRoadmapLimit().finally(() => {}); loadAIRoadmapLimit().finally(() => {});
}, },
@ -391,7 +411,6 @@ export function GenerateRoadmap(props: GenerateRoadmapProps) {
return; return;
} }
setHasSubmitted(true);
loadAIRoadmap(roadmapId).finally(() => { loadAIRoadmap(roadmapId).finally(() => {
pageProgressMessage.set(''); pageProgressMessage.set('');
}); });
@ -415,7 +434,7 @@ export function GenerateRoadmap(props: GenerateRoadmapProps) {
); );
} }
const pageUrl = `https://roadmap.sh/ai?id=${roadmapId}`; const pageUrl = `https://roadmap.sh/ai/${roadmapSlug}`;
const canGenerateMore = roadmapLimitUsed < roadmapLimit; const canGenerateMore = roadmapLimitUsed < roadmapLimit;
return ( return (
@ -530,7 +549,7 @@ export function GenerateRoadmap(props: GenerateRoadmapProps) {
)} )}
{!isAuthenticatedUser && ( {!isAuthenticatedUser && (
<button <button
className="rounded-xl border border-current px-2.5 py-0.5 text-left text-sm font-medium text-blue-500 transition-colors hover:bg-blue-500 hover:text-white sm:text-center" className="mt-2 rounded-xl border border-current px-2.5 py-0.5 text-left text-sm font-medium text-blue-500 transition-colors hover:bg-blue-500 hover:text-white sm:text-center"
onClick={showLoginPopup} onClick={showLoginPopup}
> >
Login to generate your own roadmaps Login to generate your own roadmaps

@ -3,14 +3,13 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { useKeydown } from '../../hooks/use-keydown'; import { useKeydown } from '../../hooks/use-keydown';
import { useOutsideClick } from '../../hooks/use-outside-click'; import { useOutsideClick } from '../../hooks/use-outside-click';
import { markdownToHtml } from '../../lib/markdown'; import { markdownToHtml } from '../../lib/markdown';
import {Ban, Cog, Contact, FileText, User, UserRound, X} from 'lucide-react'; import { Ban, Cog, Contact, FileText, User, UserRound, X } from 'lucide-react';
import { Spinner } from '../ReactIcons/Spinner'; import { Spinner } from '../ReactIcons/Spinner';
import type { RoadmapNodeDetails } from './GenerateRoadmap'; import type { RoadmapNodeDetails } from './GenerateRoadmap';
import { getOpenAIKey, isLoggedIn, removeAuthToken } from '../../lib/jwt'; import { getOpenAIKey, isLoggedIn, removeAuthToken } from '../../lib/jwt';
import { readAIRoadmapContentStream } from '../../helper/read-stream'; import { readAIRoadmapContentStream } from '../../helper/read-stream';
import { cn } from '../../lib/classname'; import { cn } from '../../lib/classname';
import { showLoginPopup } from '../../lib/popup'; import { showLoginPopup } from '../../lib/popup';
import { OpenAISettings } from './OpenAISettings.tsx';
type RoadmapTopicDetailProps = RoadmapNodeDetails & { type RoadmapTopicDetailProps = RoadmapNodeDetails & {
onClose?: () => void; onClose?: () => void;
@ -179,19 +178,19 @@ export function RoadmapTopicDetail(props: RoadmapTopicDetailProps) {
)} )}
{!isLoggedIn() && ( {!isLoggedIn() && (
<div className="flex h-full flex-col items-center justify-center"> <div className="flex h-full flex-col items-center justify-center">
<Contact className="h-14 w-14 text-gray-200 mb-3.5" /> <Contact className="mb-3.5 h-14 w-14 text-gray-200" />
<h2 className='font-medium text-xl'>You must be logged in</h2> <h2 className="text-xl font-medium">You must be logged in</h2>
<p className="text-base text-gray-400"> <p className="text-base text-gray-400">
Sign up or login to generate topic content. Sign up or login to generate topic content.
</p> </p>
<button <button
className="mt-3.5 text-base font-medium text-white bg-black px-3 py-2 rounded-md w-full max-w-[300px]" className="mt-3.5 w-full max-w-[300px] rounded-md bg-black px-3 py-2 text-base font-medium text-white"
onClick={showLoginPopup} onClick={showLoginPopup}
> >
Sign up / Login Sign up / Login
</button> </button>
</div> </div>
)} )}
{!isLoading && !error && ( {!isLoading && !error && (

@ -22,8 +22,9 @@ let errorMessage = '';
if (error || !roadmap) { if (error || !roadmap) {
errorMessage = error?.message || 'Error loading AI Roadmap'; errorMessage = error?.message || 'Error loading AI Roadmap';
} }
const title = roadmap?.title || 'Roadmap AI';
--- ---
<BaseLayout title='Roadmap AI'> <BaseLayout title={title}>
<GenerateRoadmap roadmapId={roadmap?.id} client:load /> <GenerateRoadmap roadmapId={roadmap?.id} client:load />
</BaseLayout> </BaseLayout>

Loading…
Cancel
Save