mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2026-03-12 17:51:53 +08:00
Compare commits
68 Commits
feat/analy
...
feat/ai-rd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41184fdb17 | ||
|
|
e4c0bad2c0 | ||
|
|
73e46ad0d6 | ||
|
|
5d82c1080f | ||
|
|
9166b56af7 | ||
|
|
231013744a | ||
|
|
b6a6d6394b | ||
|
|
ed99d97468 | ||
|
|
fd55a52839 | ||
|
|
5c423e149f | ||
|
|
9d63306fc9 | ||
|
|
1d04dd9970 | ||
|
|
ab2b7688f1 | ||
|
|
fe651b21bc | ||
|
|
d4a563fe49 | ||
|
|
7fd319a989 | ||
|
|
edaa108900 | ||
|
|
51b8e58d7c | ||
|
|
634af33756 | ||
|
|
e7277585d0 | ||
|
|
384d73363d | ||
|
|
3a18207b32 | ||
|
|
a42d9cfb64 | ||
|
|
f0c8121d42 | ||
|
|
b2b5bfc8a1 | ||
|
|
7600b1af89 | ||
|
|
3cb31da862 | ||
|
|
5f97ea8e4f | ||
|
|
7f26ce5e2f | ||
|
|
fa483763f1 | ||
|
|
90924eefd2 | ||
|
|
7db85cc91b | ||
|
|
4300d08f70 | ||
|
|
a9d8869510 | ||
|
|
6866dae012 | ||
|
|
005c66c60a | ||
|
|
4acdbaebea | ||
|
|
fae496bdb7 | ||
|
|
9a49a04934 | ||
|
|
f8e5833f6a | ||
|
|
4e0d12af7d | ||
|
|
3c785f7044 | ||
|
|
0473c8e460 | ||
|
|
09266aed5a | ||
|
|
5e48b4f7cd | ||
|
|
d95d30caf8 | ||
|
|
c72b0b2cf2 | ||
|
|
afad1ce3c3 | ||
|
|
e006f63e14 | ||
|
|
707a7242fc | ||
|
|
324b17e878 | ||
|
|
5e5f7427f9 | ||
|
|
8bf0b51065 | ||
|
|
13c55faa71 | ||
|
|
febeb6f586 | ||
|
|
9c82a4d35c | ||
|
|
4a3030948f | ||
|
|
3d012f9c64 | ||
|
|
7856a78bba | ||
|
|
7b4ced400c | ||
|
|
bd01586e8e | ||
|
|
33af126728 | ||
|
|
e48fa4f593 | ||
|
|
f00f5d16ea | ||
|
|
4fcf1f6d50 | ||
|
|
7c1c295c63 | ||
|
|
39e55a06e8 | ||
|
|
8d6facd983 |
20
src/api/ai-roadmap.ts
Normal file
20
src/api/ai-roadmap.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { type APIContext } from 'astro';
|
||||
import { api } from './api.ts';
|
||||
|
||||
export type GetAIRoadmapBySlugResponse = {
|
||||
id: string;
|
||||
term: string;
|
||||
title: string;
|
||||
data: string;
|
||||
isAuthenticatedUser: boolean;
|
||||
};
|
||||
|
||||
export function aiRoadmapApi(context: APIContext) {
|
||||
return {
|
||||
getAIRoadmapBySlug: async function (roadmapSlug: string) {
|
||||
return api(context).get<GetAIRoadmapBySlugResponse>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-ai-roadmap-by-slug/${roadmapSlug}`,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -26,7 +26,8 @@ export function AIRoadmapsList(props: AIRoadmapsListProps) {
|
||||
return (
|
||||
<ul className="mb-4 grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{roadmaps.map((roadmap) => {
|
||||
const roadmapLink = `/ai?id=${roadmap._id}`;
|
||||
const roadmapLink = `/ai/${roadmap.slug}`;
|
||||
|
||||
return (
|
||||
<a
|
||||
key={roadmap._id}
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface AIRoadmapDocument {
|
||||
term: string;
|
||||
title: string;
|
||||
data: string;
|
||||
slug: string;
|
||||
viewCount: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
|
||||
@@ -50,6 +50,7 @@ export type GetAIRoadmapLimitResponse = {
|
||||
};
|
||||
|
||||
const ROADMAP_ID_REGEX = new RegExp('@ROADMAPID:(\\w+)@');
|
||||
const ROADMAP_SLUG_REGEX = new RegExp(/@ROADMAPSLUG:([\w-]+)@/);
|
||||
|
||||
export type RoadmapNodeDetails = {
|
||||
nodeId: string;
|
||||
@@ -87,22 +88,39 @@ type GetAIRoadmapResponse = {
|
||||
data: string;
|
||||
};
|
||||
|
||||
export function GenerateRoadmap() {
|
||||
type GenerateRoadmapProps = {
|
||||
roadmapId?: string;
|
||||
slug?: string;
|
||||
isAuthenticatedUser?: boolean;
|
||||
};
|
||||
|
||||
export function GenerateRoadmap(props: GenerateRoadmapProps) {
|
||||
const {
|
||||
roadmapId: defaultRoadmapId,
|
||||
slug: defaultRoadmapSlug,
|
||||
isAuthenticatedUser = isLoggedIn(),
|
||||
} = props;
|
||||
|
||||
const roadmapContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { id: roadmapId, rc: referralCode } = getUrlParams() as {
|
||||
id: string;
|
||||
const { rc: referralCode } = getUrlParams() as {
|
||||
rc?: string;
|
||||
};
|
||||
const toast = useToast();
|
||||
|
||||
const [hasSubmitted, setHasSubmitted] = useState<boolean>(false);
|
||||
const [roadmapId, setRoadmapId] = useState<string | undefined>(
|
||||
defaultRoadmapId,
|
||||
);
|
||||
const [roadmapSlug, setRoadmapSlug] = useState<string | undefined>(
|
||||
defaultRoadmapSlug,
|
||||
);
|
||||
const [hasSubmitted, setHasSubmitted] = useState<boolean>(Boolean(roadmapId));
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingResults, setIsLoadingResults] = useState(false);
|
||||
const [roadmapTerm, setRoadmapTerm] = useState('');
|
||||
const [generatedRoadmapContent, setGeneratedRoadmapContent] = useState('');
|
||||
const [currentRoadmap, setCurrentRoadmap] =
|
||||
useState<GetAIRoadmapResponse | null>(null);
|
||||
const [generatedRoadmapContent, setGeneratedRoadmapContent] = useState('');
|
||||
const [selectedNode, setSelectedNode] = useState<RoadmapNodeDetails | null>(
|
||||
null,
|
||||
);
|
||||
@@ -117,7 +135,6 @@ export function GenerateRoadmap() {
|
||||
getOpenAIKey(),
|
||||
);
|
||||
const isKeyOnly = IS_KEY_ONLY_ROADMAP_GENERATION;
|
||||
const isAuthenticatedUser = isLoggedIn();
|
||||
|
||||
const renderRoadmap = async (roadmap: string) => {
|
||||
const { nodes, edges } = generateAIRoadmapFromText(roadmap);
|
||||
@@ -134,6 +151,7 @@ export function GenerateRoadmap() {
|
||||
deleteUrlParam('id');
|
||||
setCurrentRoadmap(null);
|
||||
|
||||
const origin = window.location.origin;
|
||||
const response = await fetch(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-roadmap`,
|
||||
{
|
||||
@@ -169,13 +187,31 @@ export function GenerateRoadmap() {
|
||||
|
||||
await readAIRoadmapStream(reader, {
|
||||
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:1234@ is the format, we will remove the token and the id
|
||||
// and replace it with a empty string
|
||||
const roadmapId = result.match(ROADMAP_ID_REGEX)?.[1] || '';
|
||||
setUrlParams({ id: roadmapId });
|
||||
result = result.replace(ROADMAP_ID_REGEX, '');
|
||||
const roadmapSlug = result.match(ROADMAP_SLUG_REGEX)?.[1] || '';
|
||||
|
||||
if (roadmapSlug) {
|
||||
window.history.pushState(
|
||||
{
|
||||
roadmapId,
|
||||
roadmapSlug,
|
||||
},
|
||||
'',
|
||||
`${origin}/ai/${roadmapSlug}`,
|
||||
);
|
||||
}
|
||||
|
||||
result = result
|
||||
.replace(ROADMAP_ID_REGEX, '')
|
||||
.replace(ROADMAP_SLUG_REGEX, '');
|
||||
|
||||
setRoadmapId(roadmapId);
|
||||
setRoadmapSlug(roadmapSlug);
|
||||
|
||||
const roadmapTitle =
|
||||
result.trim().split('\n')[0]?.replace('#', '')?.trim() || term;
|
||||
setRoadmapTerm(roadmapTitle);
|
||||
@@ -190,7 +226,10 @@ export function GenerateRoadmap() {
|
||||
await renderRoadmap(result);
|
||||
},
|
||||
onStreamEnd: async (result) => {
|
||||
result = result.replace(ROADMAP_ID_REGEX, '');
|
||||
result = result
|
||||
.replace(ROADMAP_ID_REGEX, '')
|
||||
.replace(ROADMAP_SLUG_REGEX, '');
|
||||
|
||||
setGeneratedRoadmapContent(result);
|
||||
loadAIRoadmapLimit().finally(() => {});
|
||||
},
|
||||
@@ -322,7 +361,7 @@ export function GenerateRoadmap() {
|
||||
data,
|
||||
});
|
||||
|
||||
setRoadmapTerm(term);
|
||||
setRoadmapTerm(title || term);
|
||||
setGeneratedRoadmapContent(data);
|
||||
visitAIRoadmap(roadmapId);
|
||||
};
|
||||
@@ -385,12 +424,35 @@ export function GenerateRoadmap() {
|
||||
return;
|
||||
}
|
||||
|
||||
setHasSubmitted(true);
|
||||
loadAIRoadmap(roadmapId).finally(() => {
|
||||
pageProgressMessage.set('');
|
||||
});
|
||||
}, [roadmapId, currentRoadmap]);
|
||||
|
||||
useEffect(() => {
|
||||
const handlePopState = (e: PopStateEvent) => {
|
||||
const { roadmapId, roadmapSlug } = e.state || {};
|
||||
if (!roadmapId || !roadmapSlug) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setHasSubmitted(true);
|
||||
setRoadmapId(roadmapId);
|
||||
setRoadmapSlug(roadmapSlug);
|
||||
loadAIRoadmap(roadmapId).finally(() => {
|
||||
setIsLoading(false);
|
||||
pageProgressMessage.set('');
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', handlePopState);
|
||||
return () => {
|
||||
window.removeEventListener('popstate', handlePopState);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!hasSubmitted) {
|
||||
return (
|
||||
<RoadmapSearch
|
||||
@@ -401,7 +463,7 @@ export function GenerateRoadmap() {
|
||||
limitUsed={roadmapLimitUsed}
|
||||
loadAIRoadmapLimit={loadAIRoadmapLimit}
|
||||
isKeyOnly={isKeyOnly}
|
||||
onLoadTerm={(term: string) => {
|
||||
onLoadTerm={(term) => {
|
||||
setRoadmapTerm(term);
|
||||
loadTermRoadmap(term).finally(() => {});
|
||||
}}
|
||||
@@ -409,7 +471,7 @@ export function GenerateRoadmap() {
|
||||
);
|
||||
}
|
||||
|
||||
const pageUrl = `https://roadmap.sh/ai?id=${roadmapId}`;
|
||||
const pageUrl = `https://roadmap.sh/ai/${roadmapSlug}`;
|
||||
const canGenerateMore = roadmapLimitUsed < roadmapLimit;
|
||||
|
||||
return (
|
||||
@@ -524,7 +586,7 @@ export function GenerateRoadmap() {
|
||||
)}
|
||||
{!isAuthenticatedUser && (
|
||||
<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}
|
||||
>
|
||||
Login to generate your own roadmaps
|
||||
|
||||
@@ -3,14 +3,13 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useKeydown } from '../../hooks/use-keydown';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
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 type { RoadmapNodeDetails } from './GenerateRoadmap';
|
||||
import { getOpenAIKey, isLoggedIn, removeAuthToken } from '../../lib/jwt';
|
||||
import { readAIRoadmapContentStream } from '../../helper/read-stream';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { showLoginPopup } from '../../lib/popup';
|
||||
import { OpenAISettings } from './OpenAISettings.tsx';
|
||||
|
||||
type RoadmapTopicDetailProps = RoadmapNodeDetails & {
|
||||
onClose?: () => void;
|
||||
@@ -179,19 +178,19 @@ export function RoadmapTopicDetail(props: RoadmapTopicDetailProps) {
|
||||
)}
|
||||
|
||||
{!isLoggedIn() && (
|
||||
<div className="flex h-full flex-col items-center justify-center">
|
||||
<Contact className="h-14 w-14 text-gray-200 mb-3.5" />
|
||||
<h2 className='font-medium text-xl'>You must be logged in</h2>
|
||||
<p className="text-base text-gray-400">
|
||||
Sign up or login to generate topic content.
|
||||
</p>
|
||||
<button
|
||||
className="mt-3.5 text-base font-medium text-white bg-black px-3 py-2 rounded-md w-full max-w-[300px]"
|
||||
onClick={showLoginPopup}
|
||||
>
|
||||
Sign up / Login
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex h-full flex-col items-center justify-center">
|
||||
<Contact className="mb-3.5 h-14 w-14 text-gray-200" />
|
||||
<h2 className="text-xl font-medium">You must be logged in</h2>
|
||||
<p className="text-base text-gray-400">
|
||||
Sign up or login to generate topic content.
|
||||
</p>
|
||||
<button
|
||||
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}
|
||||
>
|
||||
Sign up / Login
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && !error && (
|
||||
|
||||
34
src/pages/ai/[aiRoadmapSlug].astro
Normal file
34
src/pages/ai/[aiRoadmapSlug].astro
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
import { aiRoadmapApi } from '../../api/ai-roadmap';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { GenerateRoadmap } from '../../components/GenerateRoadmap/GenerateRoadmap';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
interface Params extends Record<string, string | undefined> {
|
||||
aiRoadmapSlug: string;
|
||||
}
|
||||
|
||||
const { aiRoadmapSlug } = Astro.params as Params;
|
||||
if (!aiRoadmapSlug) {
|
||||
return Astro.redirect('/404');
|
||||
}
|
||||
|
||||
const aiRoadmapClient = aiRoadmapApi(Astro as any);
|
||||
const { response: roadmap, error } =
|
||||
await aiRoadmapClient.getAIRoadmapBySlug(aiRoadmapSlug);
|
||||
|
||||
let errorMessage = '';
|
||||
if (error || !roadmap) {
|
||||
errorMessage = error?.message || 'Error loading AI Roadmap';
|
||||
}
|
||||
const title = roadmap?.title || 'Roadmap AI';
|
||||
---
|
||||
|
||||
<BaseLayout title={title}>
|
||||
<GenerateRoadmap
|
||||
roadmapId={roadmap?.id}
|
||||
isAuthenticatedUser={roadmap?.isAuthenticatedUser}
|
||||
client:load
|
||||
/>
|
||||
</BaseLayout>
|
||||
Reference in New Issue
Block a user