feat: implement roadmap generator

pull/5267/head
Arik Chakma 11 months ago
parent 37ffc2cc62
commit 69df6d906b
  1. 2
      package.json
  2. 131
      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. 5
      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.55)(react-dom@18.2.0)(react@18.2.0)(vite@5.0.12)
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
@ -21,8 +21,8 @@ dependencies:
specifier: ^0.7.1
version: 0.7.1(nanostores@0.9.5)(react@18.2.0)
'@types/react':
specifier: ^18.2.55
version: 18.2.55
specifier: ^18.2.56
version: 18.2.59
'@types/react-dom':
specifier: ^18.2.19
version: 18.2.19
@ -45,8 +45,8 @@ dependencies:
specifier: ^3.0.5
version: 3.0.5
lucide-react:
specifier: ^0.331.0
version: 0.331.0(react@18.2.0)
specifier: ^0.334.0
version: 0.334.0(react@18.2.0)
nanoid:
specifier: ^5.0.5
version: 5.0.5
@ -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.55)(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.0
version: 4.5.0(@types/react@18.2.55)(react@18.2.0)
specifier: ^4.5.1
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.55)(react-dom@18.2.0)(react@18.2.0)(vite@5.0.12):
/@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,9 +200,9 @@ packages:
react: ^17.0.2 || ^18.0.0
react-dom: ^17.0.2 || ^18.0.0
dependencies:
'@types/react': 18.2.55
'@types/react': 18.2.59
'@types/react-dom': 18.2.19
'@vitejs/plugin-react': 4.2.1(vite@5.0.12)
'@vitejs/plugin-react': 4.2.1(vite@5.1.3)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
ultrahtml: 1.5.2
@ -1102,39 +1108,39 @@ packages:
config-chain: 1.1.13
dev: false
/@reactflow/background@11.3.9(@types/react@18.2.55)(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.55)(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.0(@types/react@18.2.55)(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.55)(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.55)(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.0(@types/react@18.2.55)(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.55)(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.0(@types/react@18.2.55)(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.55)(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.55)(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.0(@types/react@18.2.55)(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.55)(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.55)(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.0(@types/react@18.2.55)(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.55)(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.55)(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.0(@types/react@18.2.55)(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.55
'@types/react': 18.2.59
dev: false
/@types/react@18.2.55:
resolution: {integrity: sha512-Y2Tz5P4yz23brwm2d7jNon39qoAtMMmalOQv6+fEFt1mT+FcM3D841wDpoUvFXhaYenuROCy3FZYqdTjM7qVyA==}
/@types/react@18.2.59:
resolution: {integrity: sha512-DE+F6BYEC8VtajY85Qr7mmhTd/79rJKIHCg99MU9SWPB4xvLb6D1za2vYflgZfmPqQVEr6UqJTnLXEwzpVPuOg==}
dependencies:
'@types/prop-types': 15.7.11
'@types/scheduler': 0.16.8
@ -1733,7 +1739,7 @@ packages:
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
dev: false
/@vitejs/plugin-react@4.2.1(vite@5.0.12):
/@vitejs/plugin-react@4.2.1(vite@5.1.3):
resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==}
engines: {node: ^14.18.0 || >=16.0.0}
peerDependencies:
@ -1744,7 +1750,7 @@ packages:
'@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.7)
'@types/babel__core': 7.20.5
react-refresh: 0.14.0
vite: 5.0.12
vite: 5.1.3
transitivePeerDependencies:
- supports-color
dev: false
@ -3996,8 +4002,8 @@ packages:
engines: {node: '>=12'}
dev: false
/lucide-react@0.331.0(react@18.2.0):
resolution: {integrity: sha512-CHFJ0ve9vaZ7bB2VRAl27SlX1ELh6pfNC0jS96qGpPEEzLkLDGq4pDBFU8RhOoRMqsjXqTzLm9U6bZ1OcIHq7Q==}
/lucide-react@0.334.0(react@18.2.0):
resolution: {integrity: sha512-y0Rv/Xx6qAq4FutZ3L/efl3O9vl6NC/1p0YOg6mBfRbQ4k1JCE2rz0rnV7WC8Moxq1RY99vLATvjcqUegGJTvA==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0
dependencies:
@ -5543,18 +5549,18 @@ packages:
loose-envify: 1.4.0
dev: false
/reactflow@11.10.4(@types/react@18.2.55)(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.55)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/controls': 11.2.9(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/core': 11.10.4(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/minimap': 11.7.9(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/node-resizer': 2.2.9(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/node-toolbar': 1.3.9(@types/react@18.2.55)(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:
@ -6745,41 +6751,6 @@ packages:
vfile-message: 4.0.2
dev: false
/vite@5.0.12:
resolution: {integrity: sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
'@types/node': ^18.0.0 || >=20.0.0
less: '*'
lightningcss: ^1.21.0
sass: '*'
stylus: '*'
sugarss: '*'
terser: ^5.4.0
peerDependenciesMeta:
'@types/node':
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
dependencies:
esbuild: 0.19.11
postcss: 8.4.33
rollup: 4.9.6
optionalDependencies:
fsevents: 2.3.3
dev: false
/vite@5.1.3:
resolution: {integrity: sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==}
engines: {node: ^18.0.0 || >=20.0.0}
@ -6964,8 +6935,8 @@ packages:
resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}
dev: false
/zustand@4.5.0(@types/react@18.2.55)(react@18.2.0):
resolution: {integrity: sha512-zlVFqS5TQ21nwijjhJlx4f9iGrXSL0o/+Dpy4txAP22miJ8Ti6c1Ol1RLNN98BMib83lmDH/2KmLwaNXpjrO1A==}
/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:
'@types/react': '>=16.8'
@ -6979,7 +6950,7 @@ packages:
react:
optional: true
dependencies:
'@types/react': 18.2.55
'@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