|
|
@ -15,15 +15,7 @@ import { readAIRoadmapStream } from '../../helper/read-stream'; |
|
|
|
import { isLoggedIn, removeAuthToken, visitAIRoadmap } from '../../lib/jwt'; |
|
|
|
import { isLoggedIn, removeAuthToken, visitAIRoadmap } from '../../lib/jwt'; |
|
|
|
import { RoadmapSearch } from './RoadmapSearch.tsx'; |
|
|
|
import { RoadmapSearch } from './RoadmapSearch.tsx'; |
|
|
|
import { Spinner } from '../ReactIcons/Spinner.tsx'; |
|
|
|
import { Spinner } from '../ReactIcons/Spinner.tsx'; |
|
|
|
import { |
|
|
|
import { Ban, Download, PenSquare, Save, Wand } from 'lucide-react'; |
|
|
|
BadgeCheck, |
|
|
|
|
|
|
|
Ban, |
|
|
|
|
|
|
|
Download, |
|
|
|
|
|
|
|
PenSquare, |
|
|
|
|
|
|
|
Save, |
|
|
|
|
|
|
|
Telescope, |
|
|
|
|
|
|
|
Wand, |
|
|
|
|
|
|
|
} from 'lucide-react'; |
|
|
|
|
|
|
|
import { ShareRoadmapButton } from '../ShareRoadmapButton.tsx'; |
|
|
|
import { ShareRoadmapButton } from '../ShareRoadmapButton.tsx'; |
|
|
|
import { httpGet, httpPost } from '../../lib/http.ts'; |
|
|
|
import { httpGet, httpPost } from '../../lib/http.ts'; |
|
|
|
import { pageProgressMessage } from '../../stores/page.ts'; |
|
|
|
import { pageProgressMessage } from '../../stores/page.ts'; |
|
|
@ -36,6 +28,7 @@ import { downloadGeneratedRoadmapImage } from '../../helper/download-image.ts'; |
|
|
|
import { showLoginPopup } from '../../lib/popup.ts'; |
|
|
|
import { showLoginPopup } from '../../lib/popup.ts'; |
|
|
|
import { cn } from '../../lib/classname.ts'; |
|
|
|
import { cn } from '../../lib/classname.ts'; |
|
|
|
import { RoadmapTopicDetail } from './RoadmapTopicDetail.tsx'; |
|
|
|
import { RoadmapTopicDetail } from './RoadmapTopicDetail.tsx'; |
|
|
|
|
|
|
|
import { AIRoadmapAlert } from './AIRoadmapAlert.tsx'; |
|
|
|
|
|
|
|
|
|
|
|
export type GetAIRoadmapLimitResponse = { |
|
|
|
export type GetAIRoadmapLimitResponse = { |
|
|
|
used: number; |
|
|
|
used: number; |
|
|
@ -77,7 +70,8 @@ export const allowedClickableNodeTypes = [ |
|
|
|
|
|
|
|
|
|
|
|
type GetAIRoadmapResponse = { |
|
|
|
type GetAIRoadmapResponse = { |
|
|
|
id: string; |
|
|
|
id: string; |
|
|
|
topic: string; |
|
|
|
term: string; |
|
|
|
|
|
|
|
title: string; |
|
|
|
data: string; |
|
|
|
data: string; |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
@ -89,7 +83,7 @@ export function GenerateRoadmap() { |
|
|
|
|
|
|
|
|
|
|
|
const [hasSubmitted, setHasSubmitted] = useState<boolean>(false); |
|
|
|
const [hasSubmitted, setHasSubmitted] = useState<boolean>(false); |
|
|
|
const [isLoading, setIsLoading] = useState(false); |
|
|
|
const [isLoading, setIsLoading] = useState(false); |
|
|
|
const [roadmapTopic, setRoadmapTopic] = useState(''); |
|
|
|
const [roadmapTerm, setRoadmapTerm] = useState(''); |
|
|
|
const [generatedRoadmapContent, setGeneratedRoadmapContent] = useState(''); |
|
|
|
const [generatedRoadmapContent, setGeneratedRoadmapContent] = useState(''); |
|
|
|
const [currentRoadmap, setCurrentRoadmap] = |
|
|
|
const [currentRoadmap, setCurrentRoadmap] = |
|
|
|
useState<GetAIRoadmapResponse | null>(null); |
|
|
|
useState<GetAIRoadmapResponse | null>(null); |
|
|
@ -110,7 +104,7 @@ export function GenerateRoadmap() { |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const loadTopic = async (topic: string) => { |
|
|
|
const loadTermRoadmap = async (term: string) => { |
|
|
|
setIsLoading(true); |
|
|
|
setIsLoading(true); |
|
|
|
setHasSubmitted(true); |
|
|
|
setHasSubmitted(true); |
|
|
|
|
|
|
|
|
|
|
@ -131,7 +125,7 @@ export function GenerateRoadmap() { |
|
|
|
'Content-Type': 'application/json', |
|
|
|
'Content-Type': 'application/json', |
|
|
|
}, |
|
|
|
}, |
|
|
|
credentials: 'include', |
|
|
|
credentials: 'include', |
|
|
|
body: JSON.stringify({ topic: topic }), |
|
|
|
body: JSON.stringify({ term }), |
|
|
|
}, |
|
|
|
}, |
|
|
|
); |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
@ -167,7 +161,8 @@ export function GenerateRoadmap() { |
|
|
|
result = result.replace(ROADMAP_ID_REGEX, ''); |
|
|
|
result = result.replace(ROADMAP_ID_REGEX, ''); |
|
|
|
setCurrentRoadmap({ |
|
|
|
setCurrentRoadmap({ |
|
|
|
id: roadmapId, |
|
|
|
id: roadmapId, |
|
|
|
topic: roadmapTopic, |
|
|
|
term: roadmapTerm, |
|
|
|
|
|
|
|
title: term, |
|
|
|
data: result, |
|
|
|
data: result, |
|
|
|
}); |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
@ -186,15 +181,15 @@ export function GenerateRoadmap() { |
|
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { |
|
|
|
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { |
|
|
|
e.preventDefault(); |
|
|
|
e.preventDefault(); |
|
|
|
if (!roadmapTopic) { |
|
|
|
if (!roadmapTerm) { |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (roadmapTopic === currentRoadmap?.topic) { |
|
|
|
if (roadmapTerm === currentRoadmap?.topic) { |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
loadTopic(roadmapTopic); |
|
|
|
loadTermRoadmap(roadmapTerm).finally(() => null); |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const saveAIRoadmap = async () => { |
|
|
|
const saveAIRoadmap = async () => { |
|
|
@ -212,7 +207,7 @@ export function GenerateRoadmap() { |
|
|
|
}>( |
|
|
|
}>( |
|
|
|
`${import.meta.env.PUBLIC_API_URL}/v1-save-ai-roadmap/${currentRoadmap?.id}`, |
|
|
|
`${import.meta.env.PUBLIC_API_URL}/v1-save-ai-roadmap/${currentRoadmap?.id}`, |
|
|
|
{ |
|
|
|
{ |
|
|
|
title: roadmapTopic, |
|
|
|
title: roadmapTerm, |
|
|
|
nodes: nodes.map((node) => ({ |
|
|
|
nodes: nodes.map((node) => ({ |
|
|
|
...node, |
|
|
|
...node, |
|
|
|
|
|
|
|
|
|
|
@ -252,7 +247,7 @@ export function GenerateRoadmap() { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
try { |
|
|
|
await downloadGeneratedRoadmapImage(roadmapTopic, node); |
|
|
|
await downloadGeneratedRoadmapImage(roadmapTerm, node); |
|
|
|
pageProgressMessage.set(''); |
|
|
|
pageProgressMessage.set(''); |
|
|
|
} catch (error) { |
|
|
|
} catch (error) { |
|
|
|
console.error(error); |
|
|
|
console.error(error); |
|
|
@ -291,15 +286,17 @@ export function GenerateRoadmap() { |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const { topic, data } = response; |
|
|
|
const { term, title, data } = response; |
|
|
|
await renderRoadmap(data); |
|
|
|
await renderRoadmap(data); |
|
|
|
|
|
|
|
|
|
|
|
setCurrentRoadmap({ |
|
|
|
setCurrentRoadmap({ |
|
|
|
id: roadmapId, |
|
|
|
id: roadmapId, |
|
|
|
topic, |
|
|
|
title: title, |
|
|
|
|
|
|
|
term: term, |
|
|
|
data, |
|
|
|
data, |
|
|
|
}); |
|
|
|
}); |
|
|
|
setRoadmapTopic(topic); |
|
|
|
|
|
|
|
|
|
|
|
setRoadmapTerm(title); |
|
|
|
setGeneratedRoadmapContent(data); |
|
|
|
setGeneratedRoadmapContent(data); |
|
|
|
visitAIRoadmap(roadmapId); |
|
|
|
visitAIRoadmap(roadmapId); |
|
|
|
}; |
|
|
|
}; |
|
|
@ -360,14 +357,14 @@ export function GenerateRoadmap() { |
|
|
|
if (!hasSubmitted) { |
|
|
|
if (!hasSubmitted) { |
|
|
|
return ( |
|
|
|
return ( |
|
|
|
<RoadmapSearch |
|
|
|
<RoadmapSearch |
|
|
|
roadmapTopic={roadmapTopic} |
|
|
|
roadmapTerm={roadmapTerm} |
|
|
|
setRoadmapTopic={setRoadmapTopic} |
|
|
|
setRoadmapTerm={setRoadmapTerm} |
|
|
|
handleSubmit={handleSubmit} |
|
|
|
handleSubmit={handleSubmit} |
|
|
|
limit={roadmapLimit} |
|
|
|
limit={roadmapLimit} |
|
|
|
limitUsed={roadmapLimitUsed} |
|
|
|
limitUsed={roadmapLimitUsed} |
|
|
|
onLoadTopic={(topic: string) => { |
|
|
|
onLoadTerm={(term: string) => { |
|
|
|
setRoadmapTopic(topic); |
|
|
|
setRoadmapTerm(term); |
|
|
|
loadTopic(topic).finally(() => {}); |
|
|
|
loadTermRoadmap(term).finally(() => {}); |
|
|
|
}} |
|
|
|
}} |
|
|
|
/> |
|
|
|
/> |
|
|
|
); |
|
|
|
); |
|
|
@ -406,37 +403,8 @@ export function GenerateRoadmap() { |
|
|
|
</span> |
|
|
|
</span> |
|
|
|
)} |
|
|
|
)} |
|
|
|
{!isLoading && ( |
|
|
|
{!isLoading && ( |
|
|
|
<div className="flex max-w-[750px] flex-grow flex-col items-center px-5"> |
|
|
|
<div className="container flex flex-grow flex-col items-center"> |
|
|
|
<div className="mb-3 w-full rounded-md bg-yellow-100 px-3 py-2 text-yellow-800"> |
|
|
|
<AIRoadmapAlert /> |
|
|
|
<h2 className="flex items-center text-base font-semibold text-yellow-800 sm:text-lg"> |
|
|
|
|
|
|
|
AI Generated Roadmap{' '} |
|
|
|
|
|
|
|
<span className="ml-1.5 rounded-md border border-yellow-500 bg-yellow-200 px-1.5 text-xs uppercase tracking-wide text-yellow-800"> |
|
|
|
|
|
|
|
Beta |
|
|
|
|
|
|
|
</span> |
|
|
|
|
|
|
|
</h2> |
|
|
|
|
|
|
|
<p className="mb-2 mt-1"> |
|
|
|
|
|
|
|
This is an AI generated roadmap and is not verified by{' '} |
|
|
|
|
|
|
|
<span className={'font-semibold'}>roadmap.sh</span>. We are |
|
|
|
|
|
|
|
currently in beta and working hard to improve the quality of |
|
|
|
|
|
|
|
the generated roadmaps. |
|
|
|
|
|
|
|
</p> |
|
|
|
|
|
|
|
<p className="mb-1.5 mt-2 flex gap-2 text-sm"> |
|
|
|
|
|
|
|
<a |
|
|
|
|
|
|
|
href="/ai/explore" |
|
|
|
|
|
|
|
className="flex items-center gap-1.5 rounded-md border border-yellow-600 px-2 py-1 text-yellow-700 transition-colors hover:bg-yellow-300 hover:text-yellow-800" |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
<Telescope size={15} /> |
|
|
|
|
|
|
|
Explore other AI Roadmaps |
|
|
|
|
|
|
|
</a> |
|
|
|
|
|
|
|
<a |
|
|
|
|
|
|
|
href="/roadmaps" |
|
|
|
|
|
|
|
className="flex items-center gap-1.5 rounded-md border border-yellow-600 bg-yellow-200 px-2 py-1 text-yellow-800 transition-colors hover:bg-yellow-300" |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
<BadgeCheck size={15} /> |
|
|
|
|
|
|
|
Visit Official Roadmaps |
|
|
|
|
|
|
|
</a> |
|
|
|
|
|
|
|
</p> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="mt-2 flex w-full flex-col items-start justify-between gap-2 text-sm sm:flex-row sm:items-center sm:gap-0"> |
|
|
|
<div className="mt-2 flex w-full flex-col items-start justify-between gap-2 text-sm sm:flex-row sm:items-center sm:gap-0"> |
|
|
|
<span> |
|
|
|
<span> |
|
|
|
<span |
|
|
|
<span |
|
|
@ -474,9 +442,9 @@ export function GenerateRoadmap() { |
|
|
|
autoFocus |
|
|
|
autoFocus |
|
|
|
placeholder="e.g. Try searching for Ansible or DevOps" |
|
|
|
placeholder="e.g. Try searching for Ansible or DevOps" |
|
|
|
className="flex-grow rounded-md border border-gray-400 px-3 py-2 transition-colors focus:border-black focus:outline-none" |
|
|
|
className="flex-grow rounded-md border border-gray-400 px-3 py-2 transition-colors focus:border-black focus:outline-none" |
|
|
|
value={roadmapTopic} |
|
|
|
value={roadmapTerm} |
|
|
|
onInput={(e) => |
|
|
|
onInput={(e) => |
|
|
|
setRoadmapTopic((e.target as HTMLInputElement).value) |
|
|
|
setRoadmapTerm((e.target as HTMLInputElement).value) |
|
|
|
} |
|
|
|
} |
|
|
|
/> |
|
|
|
/> |
|
|
|
<button |
|
|
|
<button |
|
|
@ -487,9 +455,9 @@ export function GenerateRoadmap() { |
|
|
|
)} |
|
|
|
)} |
|
|
|
disabled={ |
|
|
|
disabled={ |
|
|
|
!roadmapLimit || |
|
|
|
!roadmapLimit || |
|
|
|
!roadmapTopic || |
|
|
|
!roadmapTerm || |
|
|
|
roadmapLimitUsed >= roadmapLimit || |
|
|
|
roadmapLimitUsed >= roadmapLimit || |
|
|
|
roadmapTopic === currentRoadmap?.topic |
|
|
|
roadmapTerm === currentRoadmap?.term |
|
|
|
} |
|
|
|
} |
|
|
|
> |
|
|
|
> |
|
|
|
{roadmapLimit > 0 && canGenerateMore && ( |
|
|
|
{roadmapLimit > 0 && canGenerateMore && ( |
|
|
@ -520,7 +488,7 @@ export function GenerateRoadmap() { |
|
|
|
</button> |
|
|
|
</button> |
|
|
|
{roadmapId && ( |
|
|
|
{roadmapId && ( |
|
|
|
<ShareRoadmapButton |
|
|
|
<ShareRoadmapButton |
|
|
|
description={`Check out ${roadmapTopic} roadmap I generated on roadmap.sh`} |
|
|
|
description={`Check out ${roadmapTerm} roadmap I generated on roadmap.sh`} |
|
|
|
pageUrl={pageUrl} |
|
|
|
pageUrl={pageUrl} |
|
|
|
/> |
|
|
|
/> |
|
|
|
)} |
|
|
|
)} |
|
|
|