mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-24 18:03:06 +02:00
Adds AI roadmap generator (#5289)
* feat: implement roadmap generator * feat: add roadmap stream * Update UI * fix: add fingerprint visitor id * feat: implement edit generated roadmap * feat: implement ai roadmap download * feat: add limit count * fix: add limit check * fix: download image button * feat: implement roadmap generator * feat: add roadmap stream * Update UI * fix: add fingerprint visitor id * feat: implement edit generated roadmap * feat: implement ai roadmap download * feat: add limit count * fix: add limit check * fix: download image button * UI Updates * Update UI for roadmap search * Update UI for roadmap limit * Update UI for roadmap * UI responsiveness on AI roadmap generator --------- Co-authored-by: Arik Chakma <arikchangma@gmail.com>
This commit is contained in:
@@ -32,6 +32,7 @@
|
|||||||
"astro": "^4.4.0",
|
"astro": "^4.4.0",
|
||||||
"astro-compress": "^2.2.10",
|
"astro-compress": "^2.2.10",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
|
"dom-to-image": "^2.6.0",
|
||||||
"dracula-prism": "^2.1.16",
|
"dracula-prism": "^2.1.16",
|
||||||
"jose": "^5.2.2",
|
"jose": "^5.2.2",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
@@ -46,15 +47,18 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"reactflow": "^11.10.4",
|
"reactflow": "^11.10.4",
|
||||||
"rehype-external-links": "^3.0.0",
|
"rehype-external-links": "^3.0.0",
|
||||||
|
"remark-parse": "^11.0.0",
|
||||||
"roadmap-renderer": "^1.0.6",
|
"roadmap-renderer": "^1.0.6",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"tailwind-merge": "^2.2.1",
|
"tailwind-merge": "^2.2.1",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
|
"unified": "^11.0.4",
|
||||||
"zustand": "^4.5.1"
|
"zustand": "^4.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.41.2",
|
"@playwright/test": "^1.41.2",
|
||||||
"@tailwindcss/typography": "^0.5.10",
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
|
"@types/dom-to-image": "^2.6.7",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/prismjs": "^1.26.3",
|
"@types/prismjs": "^1.26.3",
|
||||||
"csv-parser": "^3.0.0",
|
"csv-parser": "^3.0.0",
|
||||||
|
90
pnpm-lock.yaml
generated
90
pnpm-lock.yaml
generated
@@ -7,7 +7,7 @@ settings:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/react':
|
'@astrojs/react':
|
||||||
specifier: ^3.0.10
|
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':
|
'@astrojs/sitemap':
|
||||||
specifier: ^3.0.5
|
specifier: ^3.0.5
|
||||||
version: 3.0.5
|
version: 3.0.5
|
||||||
@@ -22,7 +22,7 @@ dependencies:
|
|||||||
version: 0.7.1(nanostores@0.9.5)(react@18.2.0)
|
version: 0.7.1(nanostores@0.9.5)(react@18.2.0)
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^18.2.56
|
specifier: ^18.2.56
|
||||||
version: 18.2.58
|
version: 18.2.59
|
||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
specifier: ^18.2.19
|
specifier: ^18.2.19
|
||||||
version: 18.2.19
|
version: 18.2.19
|
||||||
@@ -35,6 +35,9 @@ dependencies:
|
|||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.0
|
specifier: ^2.1.0
|
||||||
version: 2.1.0
|
version: 2.1.0
|
||||||
|
dom-to-image:
|
||||||
|
specifier: ^2.6.0
|
||||||
|
version: 2.6.0
|
||||||
dracula-prism:
|
dracula-prism:
|
||||||
specifier: ^2.1.16
|
specifier: ^2.1.16
|
||||||
version: 2.1.16
|
version: 2.1.16
|
||||||
@@ -73,10 +76,13 @@ dependencies:
|
|||||||
version: 18.2.0(react@18.2.0)
|
version: 18.2.0(react@18.2.0)
|
||||||
reactflow:
|
reactflow:
|
||||||
specifier: ^11.10.4
|
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:
|
rehype-external-links:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
|
remark-parse:
|
||||||
|
specifier: ^11.0.0
|
||||||
|
version: 11.0.0
|
||||||
roadmap-renderer:
|
roadmap-renderer:
|
||||||
specifier: ^1.0.6
|
specifier: ^1.0.6
|
||||||
version: 1.0.6
|
version: 1.0.6
|
||||||
@@ -89,9 +95,12 @@ dependencies:
|
|||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^3.4.1
|
specifier: ^3.4.1
|
||||||
version: 3.4.1
|
version: 3.4.1
|
||||||
|
unified:
|
||||||
|
specifier: ^11.0.4
|
||||||
|
version: 11.0.4
|
||||||
zustand:
|
zustand:
|
||||||
specifier: ^4.5.1
|
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:
|
devDependencies:
|
||||||
'@playwright/test':
|
'@playwright/test':
|
||||||
@@ -100,6 +109,9 @@ devDependencies:
|
|||||||
'@tailwindcss/typography':
|
'@tailwindcss/typography':
|
||||||
specifier: ^0.5.10
|
specifier: ^0.5.10
|
||||||
version: 0.5.10(tailwindcss@3.4.1)
|
version: 0.5.10(tailwindcss@3.4.1)
|
||||||
|
'@types/dom-to-image':
|
||||||
|
specifier: ^2.6.7
|
||||||
|
version: 2.6.7
|
||||||
'@types/js-cookie':
|
'@types/js-cookie':
|
||||||
specifier: ^3.0.6
|
specifier: ^3.0.6
|
||||||
version: 3.0.6
|
version: 3.0.6
|
||||||
@@ -185,7 +197,7 @@ packages:
|
|||||||
prismjs: 1.29.0
|
prismjs: 1.29.0
|
||||||
dev: false
|
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==}
|
resolution: {integrity: sha512-uGRIwKMAn7tva2vxXMyoVIGxWFr0rjZ8ZWIlkTG/vIpnAjD2nM8Cz6B8j7yzj176jvl6gZ6xTbTVPm09aeK0Yw==}
|
||||||
engines: {node: '>=18.14.1'}
|
engines: {node: '>=18.14.1'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -194,7 +206,7 @@ packages:
|
|||||||
react: ^17.0.2 || ^18.0.0
|
react: ^17.0.2 || ^18.0.0
|
||||||
react-dom: ^17.0.2 || ^18.0.0
|
react-dom: ^17.0.2 || ^18.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/react': 18.2.58
|
'@types/react': 18.2.59
|
||||||
'@types/react-dom': 18.2.19
|
'@types/react-dom': 18.2.19
|
||||||
'@vitejs/plugin-react': 4.2.1(vite@5.1.3)
|
'@vitejs/plugin-react': 4.2.1(vite@5.1.3)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
@@ -1102,39 +1114,39 @@ packages:
|
|||||||
config-chain: 1.1.13
|
config-chain: 1.1.13
|
||||||
dev: false
|
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==}
|
resolution: {integrity: sha512-byj/G9pEC8tN0wT/ptcl/LkEP/BBfa33/SvBkqE4XwyofckqF87lKp573qGlisfnsijwAbpDlf81PuFL41So4Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: '>=17'
|
react: '>=17'
|
||||||
react-dom: '>=17'
|
react-dom: '>=17'
|
||||||
dependencies:
|
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
|
classcat: 5.0.4
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.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:
|
transitivePeerDependencies:
|
||||||
- '@types/react'
|
- '@types/react'
|
||||||
- immer
|
- immer
|
||||||
dev: false
|
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==}
|
resolution: {integrity: sha512-e8nWplbYfOn83KN1BrxTXS17+enLyFnjZPbyDgHSRLtI5ZGPKF/8iRXV+VXb2LFVzlu4Wh3la/pkxtfP/0aguA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: '>=17'
|
react: '>=17'
|
||||||
react-dom: '>=17'
|
react-dom: '>=17'
|
||||||
dependencies:
|
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
|
classcat: 5.0.4
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.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:
|
transitivePeerDependencies:
|
||||||
- '@types/react'
|
- '@types/react'
|
||||||
- immer
|
- immer
|
||||||
dev: false
|
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==}
|
resolution: {integrity: sha512-j3i9b2fsTX/sBbOm+RmNzYEFWbNx4jGWGuGooh2r1jQaE2eV+TLJgiG/VNOp0q5mBl9f6g1IXs3Gm86S9JfcGw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: '>=17'
|
react: '>=17'
|
||||||
@@ -1150,19 +1162,19 @@ packages:
|
|||||||
d3-zoom: 3.0.0
|
d3-zoom: 3.0.0
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.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:
|
transitivePeerDependencies:
|
||||||
- '@types/react'
|
- '@types/react'
|
||||||
- immer
|
- immer
|
||||||
dev: false
|
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==}
|
resolution: {integrity: sha512-le95jyTtt3TEtJ1qa7tZ5hyM4S7gaEQkW43cixcMOZLu33VAdc2aCpJg/fXcRrrf7moN2Mbl9WIMNXUKsp5ILA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: '>=17'
|
react: '>=17'
|
||||||
react-dom: '>=17'
|
react-dom: '>=17'
|
||||||
dependencies:
|
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-selection': 3.0.10
|
||||||
'@types/d3-zoom': 3.0.8
|
'@types/d3-zoom': 3.0.8
|
||||||
classcat: 5.0.4
|
classcat: 5.0.4
|
||||||
@@ -1170,41 +1182,41 @@ packages:
|
|||||||
d3-zoom: 3.0.0
|
d3-zoom: 3.0.0
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.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:
|
transitivePeerDependencies:
|
||||||
- '@types/react'
|
- '@types/react'
|
||||||
- immer
|
- immer
|
||||||
dev: false
|
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==}
|
resolution: {integrity: sha512-HfickMm0hPDIHt9qH997nLdgLt0kayQyslKE0RS/GZvZ4UMQJlx/NRRyj5y47Qyg0NnC66KYOQWDM9LLzRTnUg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: '>=17'
|
react: '>=17'
|
||||||
react-dom: '>=17'
|
react-dom: '>=17'
|
||||||
dependencies:
|
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
|
classcat: 5.0.4
|
||||||
d3-drag: 3.0.0
|
d3-drag: 3.0.0
|
||||||
d3-selection: 3.0.0
|
d3-selection: 3.0.0
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.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:
|
transitivePeerDependencies:
|
||||||
- '@types/react'
|
- '@types/react'
|
||||||
- immer
|
- immer
|
||||||
dev: false
|
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==}
|
resolution: {integrity: sha512-VmgxKmToax4sX1biZ9LXA7cj/TBJ+E5cklLGwquCCVVxh+lxpZGTBF3a5FJGVHiUNBBtFsC8ldcSZIK4cAlQww==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: '>=17'
|
react: '>=17'
|
||||||
react-dom: '>=17'
|
react-dom: '>=17'
|
||||||
dependencies:
|
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
|
classcat: 5.0.4
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.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:
|
transitivePeerDependencies:
|
||||||
- '@types/react'
|
- '@types/react'
|
||||||
- immer
|
- immer
|
||||||
@@ -1618,6 +1630,10 @@ packages:
|
|||||||
'@types/ms': 0.7.34
|
'@types/ms': 0.7.34
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/dom-to-image@2.6.7:
|
||||||
|
resolution: {integrity: sha512-me5VbCv+fcXozblWwG13krNBvuEOm6kA5xoa4RrjDJCNFOZSWR3/QLtOXimBHk1Fisq69Gx3JtOoXtg1N1tijg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/estree@1.0.5:
|
/@types/estree@1.0.5:
|
||||||
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
|
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -1700,11 +1716,11 @@ packages:
|
|||||||
/@types/react-dom@18.2.19:
|
/@types/react-dom@18.2.19:
|
||||||
resolution: {integrity: sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==}
|
resolution: {integrity: sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/react': 18.2.58
|
'@types/react': 18.2.59
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@types/react@18.2.58:
|
/@types/react@18.2.59:
|
||||||
resolution: {integrity: sha512-TaGvMNhxvG2Q0K0aYxiKfNDS5m5ZsoIBBbtfUorxdH4NGSXIlYvZxLJI+9Dd3KjeB3780bciLyAb7ylO8pLhPw==}
|
resolution: {integrity: sha512-DE+F6BYEC8VtajY85Qr7mmhTd/79rJKIHCg99MU9SWPB4xvLb6D1za2vYflgZfmPqQVEr6UqJTnLXEwzpVPuOg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/prop-types': 15.7.11
|
'@types/prop-types': 15.7.11
|
||||||
'@types/scheduler': 0.16.8
|
'@types/scheduler': 0.16.8
|
||||||
@@ -2697,6 +2713,10 @@ packages:
|
|||||||
entities: 4.5.0
|
entities: 4.5.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/dom-to-image@2.6.0:
|
||||||
|
resolution: {integrity: sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/domelementtype@2.3.0:
|
/domelementtype@2.3.0:
|
||||||
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -5543,18 +5563,18 @@ packages:
|
|||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
dev: false
|
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==}
|
resolution: {integrity: sha512-0CApYhtYicXEDg/x2kvUHiUk26Qur8lAtTtiSlptNKuyEuGti6P1y5cS32YGaUoDMoCqkm/m+jcKkfMOvSCVRA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: '>=17'
|
react: '>=17'
|
||||||
react-dom: '>=17'
|
react-dom: '>=17'
|
||||||
dependencies:
|
dependencies:
|
||||||
'@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)
|
||||||
'@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)
|
||||||
'@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)
|
||||||
'@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)
|
||||||
'@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)
|
||||||
'@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)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -6929,7 +6949,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}
|
resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}
|
||||||
dev: false
|
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==}
|
resolution: {integrity: sha512-XlauQmH64xXSC1qGYNv00ODaQ3B+tNPoy22jv2diYiP4eoDKr9LA+Bh5Bc3gplTrFdb6JVI+N4kc1DZ/tbtfPg==}
|
||||||
engines: {node: '>=12.7.0'}
|
engines: {node: '>=12.7.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -6944,7 +6964,7 @@ packages:
|
|||||||
react:
|
react:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/react': 18.2.58
|
'@types/react': 18.2.59
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
use-sync-external-store: 1.2.0(react@18.2.0)
|
use-sync-external-store: 1.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
BIN
public/images/icons8-wand.gif
Normal file
BIN
public/images/icons8-wand.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
58
src/components/GenerateRoadmap/GenerateRoadmap.css
Normal file
58
src/components/GenerateRoadmap/GenerateRoadmap.css
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'balsamiq';
|
||||||
|
src: url('/fonts/balsamiq.woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
361
src/components/GenerateRoadmap/GenerateRoadmap.tsx
Normal file
361
src/components/GenerateRoadmap/GenerateRoadmap.tsx
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
import { type FormEvent, useEffect, useRef, useState } from 'react';
|
||||||
|
import './GenerateRoadmap.css';
|
||||||
|
import { useToast } from '../../hooks/use-toast';
|
||||||
|
import { generateAIRoadmapFromText } from '../../../editor/utils/roadmap-generator';
|
||||||
|
import { renderFlowJSON } from '../../../editor/renderer/renderer';
|
||||||
|
import { replaceChildren } from '../../lib/dom';
|
||||||
|
import { readAIRoadmapStream } from '../../helper/read-stream';
|
||||||
|
import { isLoggedIn, removeAuthToken } from '../../lib/jwt';
|
||||||
|
import { RoadmapSearch } from './RoadmapSearch.tsx';
|
||||||
|
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||||
|
import { Ban, Download, PenSquare, Wand } from 'lucide-react';
|
||||||
|
import { ShareRoadmapButton } from '../ShareRoadmapButton.tsx';
|
||||||
|
import { httpGet, httpPost } from '../../lib/http.ts';
|
||||||
|
import { pageProgressMessage } from '../../stores/page.ts';
|
||||||
|
import {
|
||||||
|
deleteUrlParam,
|
||||||
|
getUrlParams,
|
||||||
|
setUrlParams,
|
||||||
|
} from '../../lib/browser.ts';
|
||||||
|
import { downloadGeneratedRoadmapImage } from '../../helper/download-image.ts';
|
||||||
|
import { showLoginPopup } from '../../lib/popup.ts';
|
||||||
|
import { cn } from '../../lib/classname.ts';
|
||||||
|
|
||||||
|
const ROADMAP_ID_REGEX = new RegExp('@ROADMAPID:(\\w+)@');
|
||||||
|
|
||||||
|
export function GenerateRoadmap() {
|
||||||
|
const roadmapContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const { id: roadmapId } = getUrlParams() as { id: string };
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const [hasSubmitted, setHasSubmitted] = useState<boolean>(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [roadmapTopic, setRoadmapTopic] = useState('');
|
||||||
|
const [generatedRoadmap, setGeneratedRoadmap] = useState('');
|
||||||
|
|
||||||
|
const [roadmapLimit, setRoadmapLimit] = useState(0);
|
||||||
|
const [roadmapLimitUsed, setRoadmapLimitUsed] = useState(0);
|
||||||
|
|
||||||
|
const renderRoadmap = async (roadmap: string) => {
|
||||||
|
const { nodes, edges } = generateAIRoadmapFromText(roadmap);
|
||||||
|
const svg = await renderFlowJSON({ nodes, edges });
|
||||||
|
if (roadmapContainerRef?.current) {
|
||||||
|
replaceChildren(roadmapContainerRef?.current, svg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!roadmapTopic) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setHasSubmitted(true);
|
||||||
|
|
||||||
|
if (roadmapLimitUsed >= roadmapLimit) {
|
||||||
|
toast.error('You have reached your limit of generating roadmaps');
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteUrlParam('id');
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-roadmap`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({ topic: roadmapTopic }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
toast.error(data?.message || 'Something went wrong');
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
// Logout user if token is invalid
|
||||||
|
if (data.status === 401) {
|
||||||
|
removeAuthToken();
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
|
||||||
|
if (!reader) {
|
||||||
|
setIsLoading(false);
|
||||||
|
toast.error('Something went wrong');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await readAIRoadmapStream(reader, {
|
||||||
|
onStream: async (result) => {
|
||||||
|
if (result.includes('@ROADMAPID')) {
|
||||||
|
// @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, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
await renderRoadmap(result);
|
||||||
|
},
|
||||||
|
onStreamEnd: async (result) => {
|
||||||
|
result = result.replace(ROADMAP_ID_REGEX, '');
|
||||||
|
setGeneratedRoadmap(result);
|
||||||
|
loadAIRoadmapLimit().finally(() => {});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const editGeneratedRoadmap = async () => {
|
||||||
|
if (!isLoggedIn()) {
|
||||||
|
showLoginPopup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pageProgressMessage.set('Redirecting to Editor');
|
||||||
|
|
||||||
|
const { nodes, edges } = generateAIRoadmapFromText(generatedRoadmap);
|
||||||
|
|
||||||
|
const { response, error } = await httpPost<{
|
||||||
|
roadmapId: string;
|
||||||
|
}>(`${import.meta.env.PUBLIC_API_URL}/v1-edit-ai-generated-roadmap`, {
|
||||||
|
title: roadmapTopic,
|
||||||
|
nodes: nodes.map((node) => ({
|
||||||
|
...node,
|
||||||
|
|
||||||
|
// To reset the width and height of the node
|
||||||
|
// so that it can be calculated based on the content in the editor
|
||||||
|
width: undefined,
|
||||||
|
height: undefined,
|
||||||
|
style: {
|
||||||
|
...node.style,
|
||||||
|
width: undefined,
|
||||||
|
height: undefined,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
edges,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error || !response) {
|
||||||
|
toast.error(error?.message || 'Something went wrong');
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = `${import.meta.env.PUBLIC_EDITOR_APP_URL}/${response.roadmapId}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadGeneratedRoadmap = async () => {
|
||||||
|
pageProgressMessage.set('Downloading Roadmap');
|
||||||
|
|
||||||
|
const node = document.getElementById('roadmap-container');
|
||||||
|
if (!node) {
|
||||||
|
toast.error('Something went wrong');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await downloadGeneratedRoadmapImage(roadmapTopic, node);
|
||||||
|
pageProgressMessage.set('');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error('Something went wrong');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadAIRoadmapLimit = async () => {
|
||||||
|
const { response, error } = await httpGet<{
|
||||||
|
limit: number;
|
||||||
|
used: number;
|
||||||
|
}>(`${import.meta.env.PUBLIC_API_URL}/v1-get-ai-roadmap-limit`);
|
||||||
|
|
||||||
|
if (error || !response) {
|
||||||
|
toast.error(error?.message || 'Something went wrong');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { limit, used } = response;
|
||||||
|
setRoadmapLimit(limit);
|
||||||
|
setRoadmapLimitUsed(used);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadAIRoadmap = async (roadmapId: string) => {
|
||||||
|
pageProgressMessage.set('Loading Roadmap');
|
||||||
|
|
||||||
|
const { response, error } = await httpGet<{
|
||||||
|
topic: string;
|
||||||
|
data: string;
|
||||||
|
}>(`${import.meta.env.PUBLIC_API_URL}/v1-get-ai-roadmap/${roadmapId}`);
|
||||||
|
|
||||||
|
if (error || !response) {
|
||||||
|
toast.error(error?.message || 'Something went wrong');
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { topic, data } = response;
|
||||||
|
await renderRoadmap(data);
|
||||||
|
|
||||||
|
setRoadmapTopic(topic);
|
||||||
|
setGeneratedRoadmap(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadAIRoadmapLimit().finally(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!roadmapId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasSubmitted(true);
|
||||||
|
loadAIRoadmap(roadmapId).finally(() => {
|
||||||
|
pageProgressMessage.set('');
|
||||||
|
});
|
||||||
|
}, [roadmapId]);
|
||||||
|
|
||||||
|
if (!hasSubmitted) {
|
||||||
|
return (
|
||||||
|
<RoadmapSearch
|
||||||
|
roadmapTopic={roadmapTopic}
|
||||||
|
setRoadmapTopic={setRoadmapTopic}
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
limit={roadmapLimit}
|
||||||
|
limitUsed={roadmapLimitUsed}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageUrl = `https://roadmap.sh/ai?id=${roadmapId}`;
|
||||||
|
const canGenerateMore = roadmapLimitUsed < roadmapLimit;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="flex flex-grow flex-col bg-gray-100">
|
||||||
|
<div className="flex items-center justify-center border-b bg-white py-3 sm:py-6">
|
||||||
|
{isLoading && (
|
||||||
|
<span className="flex items-center gap-2 rounded-full bg-black px-3 py-1 text-white">
|
||||||
|
<Spinner isDualRing={false} innerFill={'white'} />
|
||||||
|
Generating roadmap ..
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{!isLoading && (
|
||||||
|
<div className="flex max-w-[600px] flex-grow flex-col items-center px-5">
|
||||||
|
<div className="mt-2 flex w-full items-center justify-between text-sm">
|
||||||
|
<span className="text-gray-800">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'inline-block w-[65px] rounded-md border px-0.5 text-center text-sm tabular-nums text-gray-800',
|
||||||
|
{
|
||||||
|
'animate-pulse border-zinc-300 bg-zinc-300 text-zinc-300':
|
||||||
|
!roadmapLimit,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{roadmapLimitUsed} of {roadmapLimit}
|
||||||
|
</span>{' '}
|
||||||
|
roadmaps generated
|
||||||
|
{!isLoggedIn() && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
<button
|
||||||
|
className="font-medium text-black underline underline-offset-2"
|
||||||
|
onClick={showLoginPopup}
|
||||||
|
>
|
||||||
|
Login to increase your limit
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="my-3 flex w-full flex-col sm:flex-row sm:items-center sm:justify-center gap-2"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autoFocus
|
||||||
|
placeholder="e.g. Ansible"
|
||||||
|
className="flex-grow rounded-md border border-gray-400 px-3 py-2 transition-colors focus:border-black focus:outline-none"
|
||||||
|
value={roadmapTopic}
|
||||||
|
onInput={(e) =>
|
||||||
|
setRoadmapTopic((e.target as HTMLInputElement).value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type={'submit'}
|
||||||
|
className={cn(
|
||||||
|
'flex min-w-[127px] flex-shrink-0 items-center gap-2 rounded-md bg-black px-4 py-2 text-white justify-center',
|
||||||
|
{
|
||||||
|
'cursor-not-allowed opacity-50':
|
||||||
|
!roadmapLimit ||
|
||||||
|
!roadmapTopic ||
|
||||||
|
roadmapLimitUsed >= roadmapLimit,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{roadmapLimit > 0 && canGenerateMore && (
|
||||||
|
<>
|
||||||
|
<Wand size={20} />
|
||||||
|
Generate
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{roadmapLimit === 0 && <span>Please wait..</span>}
|
||||||
|
|
||||||
|
{roadmapLimit > 0 && !canGenerateMore && (
|
||||||
|
<span className="flex items-center text-sm">
|
||||||
|
<Ban size={15} className="mr-2" />
|
||||||
|
Limit reached
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div className="flex w-full items-center justify-between gap-2">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<button
|
||||||
|
className="inline-flex items-center justify-center gap-2 rounded-md bg-yellow-400 py-1.5 pl-2.5 pr-3 text-xs font-medium transition-opacity duration-300 hover:bg-yellow-500 sm:text-sm"
|
||||||
|
onClick={downloadGeneratedRoadmap}
|
||||||
|
>
|
||||||
|
<Download size={15} />
|
||||||
|
<span className="hidden sm:inline">Download</span>
|
||||||
|
</button>
|
||||||
|
{roadmapId && (
|
||||||
|
<ShareRoadmapButton
|
||||||
|
description={`Check out ${roadmapTopic} roadmap I generated on roadmap.sh`}
|
||||||
|
pageUrl={pageUrl}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="inline-flex items-center justify-center gap-2 rounded-md bg-gray-200 py-1.5 pl-2.5 pr-3 text-xs font-medium text-black transition-colors duration-300 hover:bg-gray-300 sm:text-sm"
|
||||||
|
onClick={editGeneratedRoadmap}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<PenSquare size={15} />
|
||||||
|
Edit in Editor
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={roadmapContainerRef}
|
||||||
|
id="roadmap-container"
|
||||||
|
className="relative px-4 py-5 [&>svg]:mx-auto [&>svg]:max-w-[1300px]"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
121
src/components/GenerateRoadmap/RoadmapSearch.tsx
Normal file
121
src/components/GenerateRoadmap/RoadmapSearch.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { Ban, Wand } from 'lucide-react';
|
||||||
|
import type { FormEvent } from 'react';
|
||||||
|
import { isLoggedIn } from '../../lib/jwt';
|
||||||
|
import { showLoginPopup } from '../../lib/popup';
|
||||||
|
import { cn } from '../../lib/classname.ts';
|
||||||
|
|
||||||
|
type RoadmapSearchProps = {
|
||||||
|
roadmapTopic: string;
|
||||||
|
setRoadmapTopic: (topic: string) => void;
|
||||||
|
handleSubmit: (e: FormEvent<HTMLFormElement>) => void;
|
||||||
|
limit: number;
|
||||||
|
limitUsed: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function RoadmapSearch(props: RoadmapSearchProps) {
|
||||||
|
const {
|
||||||
|
roadmapTopic,
|
||||||
|
setRoadmapTopic,
|
||||||
|
handleSubmit,
|
||||||
|
limit = 0,
|
||||||
|
limitUsed = 0,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const canGenerateMore = limitUsed < limit;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-grow flex-col items-center justify-center px-4 py-6 sm:px-6">
|
||||||
|
<div className="flex flex-col gap-0 text-center sm:gap-2">
|
||||||
|
<h1 className="relative text-2xl font-medium sm:text-3xl">
|
||||||
|
<span className="hidden sm:inline">Generate roadmaps with AI</span>
|
||||||
|
<span className="inline sm:hidden">AI Roadmap Generator</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-base text-gray-500 sm:text-lg">
|
||||||
|
<span className="hidden sm:inline">
|
||||||
|
Enter a topic and let the AI generate a roadmap for you
|
||||||
|
</span>
|
||||||
|
<span className="inline sm:hidden">
|
||||||
|
Enter a topic to generate a roadmap
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
if (limit > 0 && canGenerateMore) {
|
||||||
|
handleSubmit(e);
|
||||||
|
} else {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="my-3 flex w-full max-w-[600px] flex-col gap-2 sm:my-5 sm:flex-row"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g. Ansible"
|
||||||
|
className="w-full rounded-md border border-gray-400 px-3 py-2.5 transition-colors focus:border-black focus:outline-none"
|
||||||
|
value={roadmapTopic}
|
||||||
|
onInput={(e) => setRoadmapTopic((e.target as HTMLInputElement).value)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
'flex min-w-[143px] flex-shrink-0 items-center justify-center gap-2 rounded-md bg-black px-4 py-2 text-white',
|
||||||
|
{
|
||||||
|
'cursor-not-allowed opacity-50':
|
||||||
|
!limit || !roadmapTopic || limitUsed >= limit,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{limit > 0 && canGenerateMore && (
|
||||||
|
<>
|
||||||
|
<Wand size={20} />
|
||||||
|
Generate
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{limit === 0 && (
|
||||||
|
<>
|
||||||
|
<span>Please wait..</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{limit > 0 && !canGenerateMore && (
|
||||||
|
<span className="flex items-center text-base sm:text-sm">
|
||||||
|
<Ban size={15} className="mr-2" />
|
||||||
|
Limit reached
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div className="mb-36">
|
||||||
|
<p className="text-gray-500">
|
||||||
|
<span className="inline sm:hidden">Generated </span>
|
||||||
|
<span className="hidden sm:inline">You have generated </span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'inline-block w-[65px] rounded-md border px-0.5 text-center text-sm tabular-nums text-gray-800',
|
||||||
|
{
|
||||||
|
'animate-pulse border-zinc-300 bg-zinc-300 text-zinc-300':
|
||||||
|
!limit,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{limitUsed} of {limit}
|
||||||
|
</span>{' '}
|
||||||
|
roadmaps.
|
||||||
|
{!isLoggedIn && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
<button
|
||||||
|
className="font-semibold text-black underline underline-offset-2"
|
||||||
|
onClick={showLoginPopup}
|
||||||
|
>
|
||||||
|
Log in to increase your limit
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@@ -34,3 +34,35 @@ export async function downloadImage({
|
|||||||
alert('Error downloading image');
|
alert('Error downloading image');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function downloadGeneratedRoadmapImage(
|
||||||
|
name: string,
|
||||||
|
node: HTMLElement,
|
||||||
|
) {
|
||||||
|
// Append a watermark to the bottom right of the image
|
||||||
|
const watermark = document.createElement('div');
|
||||||
|
watermark.className = 'flex justify-end absolute top-4 right-4 gap-2';
|
||||||
|
watermark.innerHTML = `
|
||||||
|
<span
|
||||||
|
class='rounded-md bg-black py-2 px-2 text-white'
|
||||||
|
>
|
||||||
|
roadmap.sh
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
node.insertAdjacentElement('afterbegin', watermark);
|
||||||
|
|
||||||
|
const domtoimage = (await import('dom-to-image')).default;
|
||||||
|
if (!domtoimage) {
|
||||||
|
throw new Error('Unable to download image');
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataUrl = await domtoimage.toJpeg(node, {
|
||||||
|
bgcolor: 'white',
|
||||||
|
quality: 1,
|
||||||
|
});
|
||||||
|
node?.removeChild(watermark);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.download = `${name}-roadmap.jpg`;
|
||||||
|
link.href = dataUrl;
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
43
src/helper/read-stream.ts
Normal file
43
src/helper/read-stream.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
const NEW_LINE = '\n'.charCodeAt(0);
|
||||||
|
|
||||||
|
export async function readAIRoadmapStream(
|
||||||
|
reader: ReadableStreamDefaultReader<Uint8Array>,
|
||||||
|
{
|
||||||
|
onStream,
|
||||||
|
onStreamEnd,
|
||||||
|
}: {
|
||||||
|
onStream?: (roadmap: string) => void;
|
||||||
|
onStreamEnd?: (roadmap: string) => void;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const decoder = new TextDecoder('utf-8');
|
||||||
|
let result = '';
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { value, done } = await reader.read();
|
||||||
|
if (done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We will call the renderRoadmap callback whenever we encounter
|
||||||
|
// a new line with the result until the new line
|
||||||
|
// otherwise, we will keep appending the result to the previous result
|
||||||
|
if (value) {
|
||||||
|
let start = 0;
|
||||||
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
if (value[i] === NEW_LINE) {
|
||||||
|
result += decoder.decode(value.slice(start, i + 1));
|
||||||
|
onStream?.(result);
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (start < value.length) {
|
||||||
|
result += decoder.decode(value.slice(start));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onStream?.(result);
|
||||||
|
onStreamEnd?.(result);
|
||||||
|
reader.releaseLock();
|
||||||
|
}
|
@@ -149,7 +149,7 @@ const gaPageIdentifier = Astro.url.pathname
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class='flex min-h-screen flex-col'>
|
||||||
<slot name='page-header'>
|
<slot name='page-header'>
|
||||||
<Navigation />
|
<Navigation />
|
||||||
</slot>
|
</slot>
|
||||||
|
10
src/pages/ai/index.astro
Normal file
10
src/pages/ai/index.astro
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
import LoginPopup from '../../components/AuthenticationFlow/LoginPopup.astro';
|
||||||
|
import { GenerateRoadmap } from '../../components/GenerateRoadmap/GenerateRoadmap';
|
||||||
|
import AccountLayout from '../../layouts/AccountLayout.astro';
|
||||||
|
---
|
||||||
|
|
||||||
|
<AccountLayout title='Roadmap AI'>
|
||||||
|
<GenerateRoadmap client:load />
|
||||||
|
<LoginPopup />
|
||||||
|
</AccountLayout>
|
@@ -4,5 +4,6 @@
|
|||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"jsxImportSource": "react"
|
"jsxImportSource": "react"
|
||||||
}
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
Reference in New Issue
Block a user