feat: implement roadmap generator

ai/roadmap
Arik Chakma 8 months ago committed by Kamran Ahmed
parent 44d3724880
commit 58f12af2d7
  1. 2
      package.json
  2. 76
      pnpm-lock.yaml
  3. 53
      src/components/GenerateRoadmap/GenerateRoadmap.css
  4. 80
      src/components/GenerateRoadmap/GenerateRoadmap.tsx
  5. 8
      src/pages/ai/index.astro
  6. 3
      tsconfig.json

@ -46,10 +46,12 @@
"react-dom": "^18.2.0",
"reactflow": "^11.10.4",
"rehype-external-links": "^3.0.0",
"remark-parse": "^11.0.0",
"roadmap-renderer": "^1.0.6",
"slugify": "^1.6.6",
"tailwind-merge": "^2.2.1",
"tailwindcss": "^3.4.1",
"unified": "^11.0.4",
"zustand": "^4.5.1"
},
"devDependencies": {

@ -7,7 +7,7 @@ settings:
dependencies:
'@astrojs/react':
specifier: ^3.0.10
version: 3.0.10(@types/react-dom@18.2.19)(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0)(vite@5.1.3)
version: 3.0.10(@types/react-dom@18.2.19)(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0)(vite@5.1.3)
'@astrojs/sitemap':
specifier: ^3.0.5
version: 3.0.5
@ -22,7 +22,7 @@ dependencies:
version: 0.7.1(nanostores@0.9.5)(react@18.2.0)
'@types/react':
specifier: ^18.2.56
version: 18.2.58
version: 18.2.59
'@types/react-dom':
specifier: ^18.2.19
version: 18.2.19
@ -73,10 +73,13 @@ dependencies:
version: 18.2.0(react@18.2.0)
reactflow:
specifier: ^11.10.4
version: 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0)
version: 11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0)
rehype-external-links:
specifier: ^3.0.0
version: 3.0.0
remark-parse:
specifier: ^11.0.0
version: 11.0.0
roadmap-renderer:
specifier: ^1.0.6
version: 1.0.6
@ -89,9 +92,12 @@ dependencies:
tailwindcss:
specifier: ^3.4.1
version: 3.4.1
unified:
specifier: ^11.0.4
version: 11.0.4
zustand:
specifier: ^4.5.1
version: 4.5.1(@types/react@18.2.58)(react@18.2.0)
version: 4.5.1(@types/react@18.2.59)(react@18.2.0)
devDependencies:
'@playwright/test':
@ -185,7 +191,7 @@ packages:
prismjs: 1.29.0
dev: false
/@astrojs/react@3.0.10(@types/react-dom@18.2.19)(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0)(vite@5.1.3):
/@astrojs/react@3.0.10(@types/react-dom@18.2.19)(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0)(vite@5.1.3):
resolution: {integrity: sha512-uGRIwKMAn7tva2vxXMyoVIGxWFr0rjZ8ZWIlkTG/vIpnAjD2nM8Cz6B8j7yzj176jvl6gZ6xTbTVPm09aeK0Yw==}
engines: {node: '>=18.14.1'}
peerDependencies:
@ -194,7 +200,7 @@ packages:
react: ^17.0.2 || ^18.0.0
react-dom: ^17.0.2 || ^18.0.0
dependencies:
'@types/react': 18.2.58
'@types/react': 18.2.59
'@types/react-dom': 18.2.19
'@vitejs/plugin-react': 4.2.1(vite@5.1.3)
react: 18.2.0
@ -1102,39 +1108,39 @@ packages:
config-chain: 1.1.13
dev: false
/@reactflow/background@11.3.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0):
/@reactflow/background@11.3.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-byj/G9pEC8tN0wT/ptcl/LkEP/BBfa33/SvBkqE4XwyofckqF87lKp573qGlisfnsijwAbpDlf81PuFL41So4Q==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
dependencies:
'@reactflow/core': 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/core': 11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0)
classcat: 5.0.4
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
zustand: 4.5.1(@types/react@18.2.58)(react@18.2.0)
zustand: 4.5.1(@types/react@18.2.59)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
- immer
dev: false
/@reactflow/controls@11.2.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0):
/@reactflow/controls@11.2.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-e8nWplbYfOn83KN1BrxTXS17+enLyFnjZPbyDgHSRLtI5ZGPKF/8iRXV+VXb2LFVzlu4Wh3la/pkxtfP/0aguA==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
dependencies:
'@reactflow/core': 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/core': 11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0)
classcat: 5.0.4
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
zustand: 4.5.1(@types/react@18.2.58)(react@18.2.0)
zustand: 4.5.1(@types/react@18.2.59)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
- immer
dev: false
/@reactflow/core@11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0):
/@reactflow/core@11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-j3i9b2fsTX/sBbOm+RmNzYEFWbNx4jGWGuGooh2r1jQaE2eV+TLJgiG/VNOp0q5mBl9f6g1IXs3Gm86S9JfcGw==}
peerDependencies:
react: '>=17'
@ -1150,19 +1156,19 @@ packages:
d3-zoom: 3.0.0
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
zustand: 4.5.1(@types/react@18.2.58)(react@18.2.0)
zustand: 4.5.1(@types/react@18.2.59)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
- immer
dev: false
/@reactflow/minimap@11.7.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0):
/@reactflow/minimap@11.7.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-le95jyTtt3TEtJ1qa7tZ5hyM4S7gaEQkW43cixcMOZLu33VAdc2aCpJg/fXcRrrf7moN2Mbl9WIMNXUKsp5ILA==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
dependencies:
'@reactflow/core': 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/core': 11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0)
'@types/d3-selection': 3.0.10
'@types/d3-zoom': 3.0.8
classcat: 5.0.4
@ -1170,41 +1176,41 @@ packages:
d3-zoom: 3.0.0
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
zustand: 4.5.1(@types/react@18.2.58)(react@18.2.0)
zustand: 4.5.1(@types/react@18.2.59)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
- immer
dev: false
/@reactflow/node-resizer@2.2.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0):
/@reactflow/node-resizer@2.2.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-HfickMm0hPDIHt9qH997nLdgLt0kayQyslKE0RS/GZvZ4UMQJlx/NRRyj5y47Qyg0NnC66KYOQWDM9LLzRTnUg==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
dependencies:
'@reactflow/core': 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/core': 11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0)
classcat: 5.0.4
d3-drag: 3.0.0
d3-selection: 3.0.0
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
zustand: 4.5.1(@types/react@18.2.58)(react@18.2.0)
zustand: 4.5.1(@types/react@18.2.59)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
- immer
dev: false
/@reactflow/node-toolbar@1.3.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0):
/@reactflow/node-toolbar@1.3.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-VmgxKmToax4sX1biZ9LXA7cj/TBJ+E5cklLGwquCCVVxh+lxpZGTBF3a5FJGVHiUNBBtFsC8ldcSZIK4cAlQww==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
dependencies:
'@reactflow/core': 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/core': 11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0)
classcat: 5.0.4
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
zustand: 4.5.1(@types/react@18.2.58)(react@18.2.0)
zustand: 4.5.1(@types/react@18.2.59)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
- immer
@ -1700,11 +1706,11 @@ packages:
/@types/react-dom@18.2.19:
resolution: {integrity: sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==}
dependencies:
'@types/react': 18.2.58
'@types/react': 18.2.59
dev: false
/@types/react@18.2.58:
resolution: {integrity: sha512-TaGvMNhxvG2Q0K0aYxiKfNDS5m5ZsoIBBbtfUorxdH4NGSXIlYvZxLJI+9Dd3KjeB3780bciLyAb7ylO8pLhPw==}
/@types/react@18.2.59:
resolution: {integrity: sha512-DE+F6BYEC8VtajY85Qr7mmhTd/79rJKIHCg99MU9SWPB4xvLb6D1za2vYflgZfmPqQVEr6UqJTnLXEwzpVPuOg==}
dependencies:
'@types/prop-types': 15.7.11
'@types/scheduler': 0.16.8
@ -5543,18 +5549,18 @@ packages:
loose-envify: 1.4.0
dev: false
/reactflow@11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0):
/reactflow@11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-0CApYhtYicXEDg/x2kvUHiUk26Qur8lAtTtiSlptNKuyEuGti6P1y5cS32YGaUoDMoCqkm/m+jcKkfMOvSCVRA==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
dependencies:
'@reactflow/background': 11.3.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/controls': 11.2.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/core': 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/minimap': 11.7.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/node-resizer': 2.2.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/node-toolbar': 1.3.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/background': 11.3.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/controls': 11.2.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/core': 11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/minimap': 11.7.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/node-resizer': 2.2.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/node-toolbar': 1.3.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
transitivePeerDependencies:
@ -6929,7 +6935,7 @@ packages:
resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}
dev: false
/zustand@4.5.1(@types/react@18.2.58)(react@18.2.0):
/zustand@4.5.1(@types/react@18.2.59)(react@18.2.0):
resolution: {integrity: sha512-XlauQmH64xXSC1qGYNv00ODaQ3B+tNPoy22jv2diYiP4eoDKr9LA+Bh5Bc3gplTrFdb6JVI+N4kc1DZ/tbtfPg==}
engines: {node: '>=12.7.0'}
peerDependencies:
@ -6944,7 +6950,7 @@ packages:
react:
optional: true
dependencies:
'@types/react': 18.2.58
'@types/react': 18.2.59
react: 18.2.0
use-sync-external-store: 1.2.0(react@18.2.0)
dev: false

@ -0,0 +1,53 @@
svg text tspan {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeSpeed;
}
svg > g[data-type='topic'],
svg > g[data-type='subtopic'],
svg > g > g[data-type='link-item'],
svg > g[data-type='button'] {
cursor: pointer;
}
svg > g[data-type='topic']:hover > rect {
fill: #d6d700;
}
svg > g[data-type='subtopic']:hover > rect {
fill: #f3c950;
}
svg > g[data-type='button']:hover {
opacity: 0.8;
}
svg .done rect {
fill: #cbcbcb !important;
}
svg .done text,
svg .skipped text {
text-decoration: line-through;
}
svg > g[data-type='topic'].learning > rect + text,
svg > g[data-type='topic'].done > rect + text {
fill: black;
}
svg > g[data-type='subtipic'].done > rect + text,
svg > g[data-type='subtipic'].learning > rect + text {
fill: #cbcbcb;
}
svg .learning rect {
fill: #dad1fd !important;
}
svg .learning text {
text-decoration: underline;
}
svg .skipped rect {
fill: #496b69 !important;
}

@ -0,0 +1,80 @@
import { useRef, useState, type FormEvent } from 'react';
import './GenerateRoadmap.css';
import { httpPost } from '../../lib/http';
import { useToast } from '../../hooks/use-toast';
import { generateRoadmapFromJSON } from '../../../editor/utils/roadmap-generator';
import { renderFlowJSON } from '../../../editor/renderer/renderer';
import { replaceChildren } from '../../lib/dom';
export function GenerateRoadmap() {
const roadmapContainerRef = useRef<HTMLDivElement>(null);
const toast = useToast();
const [isLoading, setIsLoading] = useState(false);
const [roadmapName, setRoadmapName] = useState('');
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
const { response, error } = await httpPost(
`${import.meta.env.PUBLIC_API_URL}/v1-generate-roadmap`,
{
title: roadmapName,
},
);
if (error || !response) {
setIsLoading(false);
toast.error(error?.message || 'Something went wrong');
return;
}
const { nodes, edges } = generateRoadmapFromJSON(response as any);
const svg = await renderFlowJSON({ nodes, edges });
if (roadmapContainerRef?.current) {
replaceChildren(roadmapContainerRef?.current, svg);
}
setIsLoading(false);
};
return (
<section className="container grid grid-cols-[280px,1fr]">
<form
className="h-full space-y-4 border-r px-4 py-10"
onSubmit={handleSubmit}
>
<div className="flex w-full flex-col">
<label
htmlFor="roadmap-name"
className='text-sm leading-none text-slate-500 after:text-red-400 after:content-["*"]'
>
Roadmap Title
</label>
<input
type="text"
name="roadmap-name"
id="roadmap-name"
className="mt-2 block w-full appearance-none rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
required
placeholder="Frontend"
value={roadmapName}
onInput={(e) =>
setRoadmapName((e.target as HTMLInputElement).value)
}
/>
</div>
<button
type="submit"
disabled={isLoading}
className="inline-flex w-full items-center justify-center rounded-lg bg-black p-2 py-3 text-sm font-medium text-white outline-none focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-400"
>
{isLoading ? 'Please wait...' : 'Generate'}
</button>
</form>
<div ref={roadmapContainerRef} className="px-4 py-10" />
</section>
);
}

@ -0,0 +1,8 @@
---
import { GenerateRoadmap } from '../../components/GenerateRoadmap/GenerateRoadmap';
import AccountLayout from '../../layouts/AccountLayout.astro';
---
<AccountLayout title='Roadmap AI'>
<GenerateRoadmap client:load />
</AccountLayout>

@ -4,5 +4,6 @@
"moduleResolution": "node",
"jsx": "react-jsx",
"jsxImportSource": "react"
}
},
"exclude": ["node_modules", "dist"]
}
Loading…
Cancel
Save