Compare commits

..

112 Commits

Author SHA1 Message Date
Arik Chakma
d52e6ffccc fix: duplicate guides 2025-09-03 18:03:24 +06:00
Obscure octopus
3ca9f81298 Update learning resource (#9091)
Included a updated version of git & github crash course as earlier version was 8 years old
2025-09-03 12:52:14 +01:00
shreyazh
56c4630e0d Fix typo (#9100)
Corrected spelling from WHow to How
2025-09-03 12:50:07 +01:00
Daniel Wolff
36af3ddcf1 Fix typo (#9101)
Fixed the name of the tool (Perfect->Prefect)

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2025-09-03 12:49:48 +01:00
github-actions[bot]
0e7afe3c99 chore: sync content to repository - nextjs (#9098)
* chore: sync content to repo

* Update src/data/roadmaps/nextjs/content/adapters@fXXlJ6oN_YPWVr-fqEar3.md

---------

Co-authored-by: kamranahmedse <4921183+kamranahmedse@users.noreply.github.com>
Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2025-09-03 12:44:49 +01:00
kamranahmedse
b605fd6337 chore: sync content to repo 2025-09-03 12:43:21 +01:00
kamranahmedse
ba1e5a58b5 chore: sync content to repo 2025-09-03 12:43:01 +01:00
Arik Chakma
dd12cf1c99 fix: remove log 2025-09-03 11:02:46 +01:00
Arik Chakma
44854cc5fb fix: official roadmap json 2025-09-03 11:02:46 +01:00
Arik Chakma
b1e60f1614 fix: beginner roadmaps 2025-09-02 17:54:54 +01:00
Arik Chakma
168ad05afe fix: project card 2025-09-02 17:54:54 +01:00
Arik Chakma
bb0419bf8a feat: official project 2025-09-02 17:54:54 +01:00
Kamran Ahmed
2d18cefd55 Revert "Revert "feat: official roadmap meta"" (#9096)
* Revert "Revert "chore: update roadmap json endpoint""

This reverts commit 8dbe1468ed.

* Revert "Revert "feat: roadmap main page""

This reverts commit bb13bf38a8.

* Revert "Revert "chore: replace roadmap listing""

This reverts commit 80dfd5b206.

* Revert "Revert "feat: roadmap courses""

This reverts commit a89c2d454f.

* Revert "Revert "fix: course length""

This reverts commit d1cf7cca99.

* Revert "Revert "feat: roadmap with courses""

This reverts commit 9c32f9d469.

* Revert "Revert "chore: disable pre-render for roadmaps""

This reverts commit cef4c29f10.
2025-09-01 20:22:54 +01:00
Arik Chakma
931e1b4a31 fix: rename key 2025-09-01 20:12:50 +01:00
Arik Chakma
e2075529ac feat: add roadmap key 2025-09-01 20:12:50 +01:00
Kamran Ahmed
8dbe1468ed Revert "chore: update roadmap json endpoint"
This reverts commit 580e764097.
2025-09-01 18:56:02 +01:00
Kamran Ahmed
bb13bf38a8 Revert "feat: roadmap main page"
This reverts commit ffb1cb5059.
2025-09-01 18:56:02 +01:00
Kamran Ahmed
80dfd5b206 Revert "chore: replace roadmap listing"
This reverts commit c4c28944ee.
2025-09-01 18:56:02 +01:00
Kamran Ahmed
a89c2d454f Revert "feat: roadmap courses"
This reverts commit f9f38101f9.
2025-09-01 18:56:02 +01:00
Kamran Ahmed
d1cf7cca99 Revert "fix: course length"
This reverts commit 40c7ea1b43.
2025-09-01 18:56:02 +01:00
Kamran Ahmed
9c32f9d469 Revert "feat: roadmap with courses"
This reverts commit 4e569df2a3.
2025-09-01 18:56:02 +01:00
Kamran Ahmed
cef4c29f10 Revert "chore: disable pre-render for roadmaps"
This reverts commit 679e29d12d.
2025-09-01 18:56:02 +01:00
Arik Chakma
679e29d12d chore: disable pre-render for roadmaps 2025-09-01 18:11:04 +01:00
Arik Chakma
4e569df2a3 feat: roadmap with courses 2025-09-01 18:11:04 +01:00
Arik Chakma
40c7ea1b43 fix: course length 2025-09-01 18:11:04 +01:00
Arik Chakma
f9f38101f9 feat: roadmap courses 2025-09-01 18:11:04 +01:00
Arik Chakma
c4c28944ee chore: replace roadmap listing 2025-09-01 18:11:04 +01:00
Arik Chakma
ffb1cb5059 feat: roadmap main page 2025-09-01 18:11:04 +01:00
Arik Chakma
580e764097 chore: update roadmap json endpoint 2025-09-01 18:11:04 +01:00
github-actions[bot]
111a97bb55 chore: sync content to repo (#9076)
Co-authored-by: kamranahmedse <4921183+kamranahmedse@users.noreply.github.com>
2025-09-01 17:15:22 +06:00
kamranahmedse
5d85495d72 chore: sync content to repo 2025-08-28 15:01:48 +01:00
Kamran Ahmed
ed2a251de4 Add content to Data Engineer Roadmap (#9016)
* Add basic content

* add content to data engineer roadmap

* add content to DE roadmap and fix some typos in content appearing in several roadmaps

* batch of new content for data engineer roadmap

* new batch of content from DE roadmap

* new batch in DE roadmap with 25 contents

* add 30 new content for DE roadmap

* new 30 contents for DE roadmap

* add last batch of content for DE roadmap. Ready to PR

* add 4 missing contents

* clean typo in de roadmap

---------

Co-authored-by: Javi Canales <javicanales@Dans-Laptop.local>
2025-08-28 14:59:59 +01:00
Kamran Ahmed
449e8f12e4 Add nextjs roadmap 2025-08-27 03:10:03 +01:00
Kamran Ahmed
a15b13cedd Add nextjs assets 2025-08-27 03:07:21 +01:00
Kamran Ahmed
609683db2f Add nextjs roadmap 2025-08-27 03:05:47 +01:00
Arik Chakma
3e21d05767 chore: event on limit exceed (#9069) 2025-08-25 20:52:47 +01:00
Sara Montemaggi
82edfba6e9 Add exception handling resource (#9073)
Added link to well made introductory tutorial on the topic of exception handling in Java
2025-08-25 17:59:01 +01:00
Dr. Lloyd
65d7a737ac Add ansible resource (#9078)
* Update ansible@h9vVPOmdUSeEGVQQaSTH5.md

This a full Ansible Course I took and it really helped me to upskill. I believe the community will benefit a lot from this course.

* Update src/data/roadmaps/devops/content/ansible@h9vVPOmdUSeEGVQQaSTH5.md

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2025-08-25 17:58:03 +01:00
Kamran Ahmed
2e0a69ad72 Fix changelog issue 2025-08-22 17:11:37 +01:00
Arik Chakma
485ffcf755 feat: not found topics 2025-08-22 15:53:55 +01:00
kamranahmedse
12ae7de3c5 chore: sync content to repo 2025-08-21 18:14:25 +01:00
Kamran Ahmed
9316d4027f Add BI analyst roadmap 2025-08-21 17:47:33 +01:00
Kamran Ahmed
5a63432412 Add BI analyst 2025-08-21 17:45:26 +01:00
kamranahmedse
ffecb5ae1a chore: sync content to repo 2025-08-21 16:21:29 +01:00
Kamran Ahmed
7a51c1af6c fix: broken syntax of workflow 2025-08-21 16:19:03 +01:00
Arik Chakma
6970cccc85 chore: add kamran 2025-08-21 16:04:23 +01:00
Arik Chakma
78940d44a9 fix: replace sync endpoint 2025-08-21 16:04:23 +01:00
Arik Chakma
6f11403a41 feat: migrate content to database 2025-08-21 16:04:23 +01:00
Arik Chakma
214799b0c2 chore: replace topic content 2025-08-21 16:04:23 +01:00
Arik Chakma
b5f564cba4 chore: add javi as reviewers 2025-08-21 16:04:23 +01:00
Kamran Ahmed
df53280ee9 Fix broken build 2025-08-20 23:29:32 +01:00
Kamran Ahmed
487a6a222b Pull changelog from backoffice 2025-08-20 23:20:37 +01:00
Kamran Ahmed
7933e222ee Remove guides and outdated functionality (#9055) 2025-08-20 17:54:08 +01:00
github-actions[bot]
e7b8c033fb chore: sync content to repo (#9062)
Co-authored-by: kamranahmedse <4921183+kamranahmedse@users.noreply.github.com>
2025-08-20 17:32:43 +01:00
AhfricanAce
d893d0fe5d Update stdout--stdin--stderr@t3fxSgCgtxuMtHjclPHA6.md (#9052)
It's best advised to refer to the three main basic objects of the STDIO as "Data streams".
2025-08-20 16:07:08 +01:00
github-actions[bot]
1c8571e484 chore: sync content to repo (#9061)
Co-authored-by: kamranahmedse <4921183+kamranahmedse@users.noreply.github.com>
2025-08-20 16:06:54 +01:00
Arik Chakma
3b43ed33c1 chore: linear algebra content (#9060) 2025-08-20 20:43:26 +06:00
github-actions[bot]
8a276d8e04 chore: sync content to repo (#9059)
Co-authored-by: kamranahmedse <4921183+kamranahmedse@users.noreply.github.com>
2025-08-20 20:39:57 +06:00
Arik Chakma
36a9e987b5 fix: sync content to database (#9058) 2025-08-20 20:36:47 +06:00
github-actions[bot]
402104665e chore: sync content to repo (#9057)
Co-authored-by: kamranahmedse <4921183+kamranahmedse@users.noreply.github.com>
2025-08-20 15:17:39 +01:00
Arik Chakma
9ec3c1fb9d fix: topic path 2025-08-20 15:09:14 +01:00
Arik Chakma
179cefe4da fix: remove title 2025-08-20 15:09:14 +01:00
Arik Chakma
93c1ea0496 fix: sync content description 2025-08-20 15:09:14 +01:00
Kamran Ahmed
cb7c13fd1b Make sync to not run for github actions 2025-08-20 14:24:21 +01:00
github-actions[bot]
704657cb36 Add content to Machine Learning (#9054)
* chore: sync content to repo

* Update src/data/roadmaps/machine-learning/introduction@MEL6y3vwiqwAV6FQihF34.md

* Update src/data/roadmaps/machine-learning/what-is-an-ml-engineer@FgzPlLUfGdlZPvPku0-Xl.md

---------

Co-authored-by: kamranahmedse <4921183+kamranahmedse@users.noreply.github.com>
Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2025-08-20 14:21:03 +01:00
Kamran Ahmed
eba3a78c70 Update .github/workflows/sync-content-to-repo.yml 2025-08-20 13:49:19 +01:00
Kamran Ahmed
d6cf9eb66d Update .github/workflows/sync-content-to-repo.yml 2025-08-20 13:49:19 +01:00
Arik Chakma
885e95399e fix: sync repo to db 2025-08-20 13:49:19 +01:00
Arik Chakma
d70582411e chore: sync repo to database 2025-08-20 13:49:19 +01:00
Arik Chakma
07277708eb fix: replace the api endpoint 2025-08-20 13:49:19 +01:00
Arik Chakma
87280b4c9e chore: sync content to repo 2025-08-20 13:49:19 +01:00
Kamran Ahmed
91b0a232ab Fix typos 2025-08-20 13:02:32 +01:00
Kamran Ahmed
bbedfec17d Fix AI course generator issue 2025-08-19 18:38:50 +01:00
Kamran Ahmed
96b2eb2797 Add machine learning roadmap to readme 2025-08-19 17:39:31 +01:00
Kamran Ahmed
fc1f666daf Add machine learning roadmap links 2025-08-19 17:36:03 +01:00
Kamran Ahmed
8fb38ae944 Add machine learning roadmap 2025-08-19 17:33:28 +01:00
Kamran Ahmed
bfe340508c Add machine learning roadmap 2025-08-19 17:30:32 +01:00
Arik Chakma
fc260ec3f0 chore: add data engineer 2025-08-19 17:14:45 +01:00
Arik Chakma
cd18dbad95 chore: add data engineer roadmap 2025-08-19 17:14:45 +01:00
Arik Chakma
949ada2fda fix: ai roadmap url 2025-08-19 17:14:45 +01:00
Omprakash Rawat
2823038d79 add Distributed Systems topic with resources (#9050) 2025-08-19 15:31:35 +01:00
Arik Chakma
dbb25ca129 fix: guides pages (#9048) 2025-08-19 15:29:28 +01:00
Arik Chakma
467581bbf4 chore: remove old ai pages (#9049) 2025-08-19 15:28:59 +01:00
Kamran Ahmed
bd7cf6e4d7 Add kubernetes ci/cd tools 2025-08-19 15:25:14 +01:00
Kamran Ahmed
12dd62fbeb Add FAQ to JS roadmap 2025-08-18 21:51:19 +01:00
Kamran Ahmed
10e179345c Fix dashboard for logged in users 2025-08-18 21:42:33 +01:00
Kamran Ahmed
830d365f3b Fix empty guides listing on dashboard 2025-08-18 21:25:53 +01:00
Kamran Ahmed
50b04042ee Add internal FAQ 2025-08-18 21:19:08 +01:00
Kamran Ahmed
e471c8b393 Fix table of contents issue 2025-08-18 18:02:04 +01:00
Julian Gödde
a63eb8e934 fix link to UX design roadmap (#9046) 2025-08-18 17:59:32 +01:00
Arik Chakma
f79d8c0562 refactor: roadmap specific guides (#9043)
* fix: ai course generate url

* wip

* wip

* wip

* feat: roadmap guides

* wip

* wip

* feat: featured guide list
2025-08-18 16:32:31 +01:00
Oleksandr Redko
a024a573fe fix: capitalization of "GitHub" and "GitLab" (#8885) 2025-08-18 13:32:53 +01:00
Sulfikar Alijun
b01adcc62e Add CSS resource (#9045) 2025-08-18 13:31:57 +01:00
Kamran Ahmed
a313552721 Fix broken syntax files 2025-08-15 22:25:01 +01:00
Kamran Ahmed
4931ba060f Fix syntax issue 2025-08-15 21:24:05 +01:00
Arik Chakma
bb47e557c6 fix: ai course generate url (#9023) 2025-08-14 14:04:33 +01:00
Kamran Ahmed
f0a5853058 Add content for devops and kubernetes nodes 2025-08-13 20:39:59 +01:00
Kamran Ahmed
7072431723 Add octopus deploy topic 2025-08-13 20:24:51 +01:00
github-actions[bot]
79f9e72a9d chore: update roadmap content json (#9015)
Co-authored-by: kamranahmedse <4921183+kamranahmedse@users.noreply.github.com>
2025-08-13 01:49:19 +01:00
Andrew DeVries
b9502b8256 Add content to Rust roadmap (#9014)
these articles break down Options, Results, and when to panic, all important parts of writing robust rust code
2025-08-13 01:49:03 +01:00
Kamran Ahmed
a25dced848 Update roadmap link in readme 2025-08-13 01:44:54 +01:00
Kamran Ahmed
0aab2a24b8 Add data engineer roadmap to readme 2025-08-13 01:41:49 +01:00
Kamran Ahmed
6894e73781 Update data engineer roadmap content 2025-08-13 01:39:12 +01:00
Kamran Ahmed
c1d3db0c97 Add data engineer roadmap dirs 2025-08-13 01:35:33 +01:00
Mahan Mashoof
dc8ad22192 add video @ docker/underlying-technologies (#9007)
* add video @ docker/underlying-technologies

* fix: update video name
2025-08-12 14:03:02 +01:00
Soumik Sarker
df1cdde166 fix: sql operators resource (#9009)
Signed-off-by: Soumik Sarker <ronodhirsoumik@gmail.com>
2025-08-12 14:01:21 +01:00
Andrii Sozonik
dfb3238097 fix: typo "serviece" to "service" 2025-08-12 14:00:15 +01:00
Arik Chakma
4fcff0c593 fix: roadmap chat url 2025-08-12 13:45:22 +01:00
Arik Chakma
07b85c032a refactor: floating and topic ai 2025-08-12 13:45:22 +01:00
Arik Chakma
20c1a54198 chore: add short title 2025-08-12 13:45:22 +01:00
github-actions[bot]
0a4d6871db chore: update roadmap content json (#9004)
Co-authored-by: kamranahmedse <4921183+kamranahmedse@users.noreply.github.com>
2025-08-09 07:52:37 +06:00
Javier Canales
aeda7a369c remove paid resource in Vim DevOps Roadmap (#9006)
* remove paid resource

* add Vim book

* add book label

---------

Co-authored-by: Javi Canales <javicanales@Dans-Laptop.local>
2025-08-08 16:27:08 +01:00
1167 changed files with 35298 additions and 28317 deletions

View File

@@ -3,6 +3,6 @@
"enabled": false
},
"_variables": {
"lastUpdateCheck": 1753810743067
"lastUpdateCheck": 1756224238932
}
}

View File

@@ -7,4 +7,6 @@ PUBLIC_STRIPE_INDIVIDUAL_MONTHLY_PRICE_ID=
PUBLIC_STRIPE_INDIVIDUAL_YEARLY_PRICE_ID=
PUBLIC_STRIPE_INDIVIDUAL_MONTHLY_PRICE_AMOUNT=10
PUBLIC_STRIPE_INDIVIDUAL_YEARLY_PRICE_AMOUNT=100
PUBLIC_STRIPE_INDIVIDUAL_YEARLY_PRICE_AMOUNT=100
ROADMAP_API_KEY=

View File

@@ -0,0 +1,66 @@
name: Sync Content to Repo
on:
workflow_dispatch:
inputs:
roadmap_slug:
description: "The ID of the roadmap to sync"
required: true
default: "__default__"
jobs:
sync-content:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup pnpm@v9
uses: pnpm/action-setup@v4
with:
version: 9
run_install: false
- name: Setup Node.js Version 20 (LTS)
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install Dependencies and Sync Content
run: |
echo "Installing Dependencies"
pnpm install
echo "Syncing Content to Repo"
npm run sync:content-to-repo -- --roadmap-slug=${{ inputs.roadmap_slug }} --secret=${{ secrets.GH_SYNC_SECRET }}
- name: Check for changes
id: verify-changed-files
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "changed=true" >> $GITHUB_OUTPUT
else
echo "changed=false" >> $GITHUB_OUTPUT
fi
- name: Create PR
if: steps.verify-changed-files.outputs.changed == 'true'
uses: peter-evans/create-pull-request@v7
with:
delete-branch: false
branch: "chore/sync-content-to-repo-${{ inputs.roadmap_slug }}"
base: "master"
labels: |
automated pr
reviewers: jcanalesluna,kamranahmedse
commit-message: "chore: sync content to repo"
title: "chore: sync content to repository - ${{ inputs.roadmap_slug }}"
body: |
## Sync Content to Repo
> [!IMPORTANT]
> This PR Syncs the Content to the Repo for the Roadmap: ${{ inputs.roadmap_slug }}
>
> Commit: ${{ github.sha }}
> Workflow Path: ${{ github.workflow_ref }}
**Please Review the Changes and Merge the PR if everything is fine.**

View File

@@ -0,0 +1,67 @@
name: Sync on Roadmap Changes
on:
push:
branches:
- master
paths:
- 'src/data/roadmaps/**'
jobs:
sync-on-changes:
runs-on: ubuntu-latest
if: github.actor != 'github-actions[bot]' && github.actor != 'dependabot[bot]'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # Fetch previous commit to compare changes
- name: Setup pnpm@v9
uses: pnpm/action-setup@v4
with:
version: 9
run_install: false
- name: Setup Node.js Version 20 (LTS)
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Get changed files
id: changed-files
run: |
echo "Getting changed files in /src/data/roadmaps/"
# Get changed files between HEAD and previous commit
CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD -- src/data/roadmaps/)
if [ -z "$CHANGED_FILES" ]; then
echo "No changes found in roadmaps directory"
echo "has_changes=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "Changed files:"
echo "$CHANGED_FILES"
# Convert to space-separated list for the script
CHANGED_FILES_LIST=$(echo "$CHANGED_FILES" | tr '\n' ',')
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "changed_files=$CHANGED_FILES_LIST" >> $GITHUB_OUTPUT
- name: Install Dependencies
if: steps.changed-files.outputs.has_changes == 'true'
run: |
echo "Installing Dependencies"
pnpm install
- name: Run sync script with changed files
if: steps.changed-files.outputs.has_changes == 'true'
run: |
echo "Running sync script for changed roadmap files"
echo "Changed files: ${{ steps.changed-files.outputs.changed_files }}"
# Run your script with the changed file paths
npm run sync:repo-to-database -- --files="${{ steps.changed-files.outputs.changed_files }}" --secret=${{ secrets.GH_SYNC_SECRET }}

View File

@@ -29,9 +29,13 @@
"compress:images": "tsx ./scripts/compress-images.ts",
"generate:roadmap-content-json": "tsx ./scripts/editor-roadmap-content-json.ts",
"migrate:editor-roadmaps": "tsx ./scripts/migrate-editor-roadmap.ts",
"sync:content-to-repo": "tsx ./scripts/sync-content-to-repo.ts",
"sync:repo-to-database": "tsx ./scripts/sync-repo-to-database.ts",
"migrate:content-repo-to-database": "tsx ./scripts/migrate-content-repo-to-database.ts",
"test:e2e": "playwright test"
},
"dependencies": {
"@ai-sdk/react": "2.0.0-beta.34",
"@astrojs/node": "^9.2.1",
"@astrojs/react": "^4.2.7",
"@astrojs/sitemap": "^3.4.0",
@@ -43,6 +47,7 @@
"@radix-ui/react-popover": "^1.1.14",
"@resvg/resvg-js": "^2.6.2",
"@roadmapsh/editor": "workspace:*",
"@shikijs/transformers": "^3.9.2",
"@tailwindcss/vite": "^4.1.7",
"@tanstack/react-query": "^5.76.1",
"@tiptap/core": "^2.12.0",
@@ -65,6 +70,7 @@
"image-size": "^2.0.2",
"jose": "^6.0.11",
"js-cookie": "^3.0.5",
"katex": "^0.16.22",
"lucide-react": "^0.511.0",
"luxon": "^3.6.1",
"markdown-it-async": "^2.2.0",
@@ -80,10 +86,14 @@
"react-confetti": "^6.4.0",
"react-dom": "^19.1.0",
"react-dropzone": "^14.3.8",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^3.0.2",
"react-textarea-autosize": "^8.5.9",
"react-tooltip": "^5.28.1",
"rehype-external-links": "^3.0.0",
"rehype-katex": "^7.0.1",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"remark-parse": "^11.0.0",
"roadmap-renderer": "^1.0.7",
"sanitize-html": "^2.17.0",
@@ -98,6 +108,7 @@
"tiptap-markdown": "^0.8.10",
"turndown": "^7.2.0",
"unified": "^11.0.5",
"zod": "^4.0.17",
"zustand": "^5.0.4"
},
"devDependencies": {
@@ -113,7 +124,7 @@
"@types/react-slick": "^0.23.13",
"@types/sanitize-html": "^2.16.0",
"@types/turndown": "^5.0.5",
"ai": "^4.3.16",
"ai": "5.0.0-beta.34",
"csv-parser": "^3.2.0",
"gh-pages": "^6.3.0",
"js-yaml": "^4.1.0",

474
pnpm-lock.yaml generated
View File

@@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@ai-sdk/react':
specifier: 2.0.0-beta.34
version: 2.0.0-beta.34(react@19.1.0)(zod@4.0.17)
'@astrojs/node':
specifier: ^9.2.1
version: 9.2.1(astro@5.7.13(@types/node@22.15.17)(jiti@2.4.2)(lightningcss@1.30.1)(rollup@4.40.2)(tsx@4.19.4)(typescript@5.8.3))
@@ -41,6 +44,9 @@ importers:
'@roadmapsh/editor':
specifier: workspace:*
version: link:packages/editor
'@shikijs/transformers':
specifier: ^3.9.2
version: 3.9.2
'@tailwindcss/vite':
specifier: ^4.1.7
version: 4.1.7(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4))
@@ -107,6 +113,9 @@ importers:
js-cookie:
specifier: ^3.0.5
version: 3.0.5
katex:
specifier: ^0.16.22
version: 0.16.22
lucide-react:
specifier: ^0.511.0
version: 0.511.0(react@19.1.0)
@@ -152,6 +161,9 @@ importers:
react-dropzone:
specifier: ^14.3.8
version: 14.3.8(react@19.1.0)
react-markdown:
specifier: ^10.1.0
version: 10.1.0(@types/react@19.1.4)(react@19.1.0)
react-resizable-panels:
specifier: ^3.0.2
version: 3.0.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -164,6 +176,15 @@ importers:
rehype-external-links:
specifier: ^3.0.0
version: 3.0.0
rehype-katex:
specifier: ^7.0.1
version: 7.0.1
remark-gfm:
specifier: ^4.0.1
version: 4.0.1
remark-math:
specifier: ^6.0.0
version: 6.0.0
remark-parse:
specifier: ^11.0.0
version: 11.0.0
@@ -206,13 +227,16 @@ importers:
unified:
specifier: ^11.0.5
version: 11.0.5
zod:
specifier: ^4.0.17
version: 4.0.17
zustand:
specifier: ^5.0.4
version: 5.0.4(@types/react@19.1.4)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0))
devDependencies:
'@ai-sdk/google':
specifier: ^1.2.18
version: 1.2.18(zod@3.24.4)
version: 1.2.18(zod@4.0.17)
'@playwright/test':
specifier: ^1.52.0
version: 1.52.0
@@ -247,8 +271,8 @@ importers:
specifier: ^5.0.5
version: 5.0.5
ai:
specifier: ^4.3.16
version: 4.3.16(react@19.1.0)(zod@3.24.4)
specifier: 5.0.0-beta.34
version: 5.0.0-beta.34(zod@4.0.17)
csv-parser:
specifier: ^3.2.0
version: 3.2.0
@@ -263,7 +287,7 @@ importers:
version: 14.1.0
openai:
specifier: ^4.100.0
version: 4.100.0(zod@3.24.4)
version: 4.100.0(zod@4.0.17)
prettier:
specifier: ^3.5.3
version: 3.5.3
@@ -334,6 +358,12 @@ importers:
packages:
'@ai-sdk/gateway@1.0.0-beta.19':
resolution: {integrity: sha512-felWPMuECZRGx8xnmvH5dW3jywKTkGnw/tXN8szphGzEDr/BfxywuXijfPBG2WBUS6frPXsvSLDRdCm5W38PXA==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4
'@ai-sdk/google@1.2.18':
resolution: {integrity: sha512-8B70+i+uB12Ae6Sn6B9Oc6W0W/XorGgc88Nx0pyUrcxFOdytHBaAVhTPqYsO3LLClfjYN8pQ9GMxd5cpGEnUcA==}
engines: {node: '>=18'}
@@ -346,26 +376,30 @@ packages:
peerDependencies:
zod: ^3.23.8
'@ai-sdk/provider-utils@3.0.0-beta.10':
resolution: {integrity: sha512-e6WSsgM01au04/1L/v5daXHn00eKjPBQXl3jq3BfvQbQ1jo8Rls2pvrdkyVc25jBW4TV4Zm+tw+v6NAh5NPXMA==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4
'@ai-sdk/provider@1.1.3':
resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==}
engines: {node: '>=18'}
'@ai-sdk/react@1.2.12':
resolution: {integrity: sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==}
'@ai-sdk/provider@2.0.0-beta.2':
resolution: {integrity: sha512-vqhtZA7R24q1XnmfmIb1fZSmHMIaJH1BVQ+0kFnNJgqWsc+V8i+yfetZ37gUc4fXATFmBuS/6O7+RPoHsZ2Fqg==}
engines: {node: '>=18'}
'@ai-sdk/react@2.0.0-beta.34':
resolution: {integrity: sha512-6v55iQbJRJ42nFM7GPzmzaP3NxEgFamKQu2fYc8jl5McQyYka3gZ7jHpy4jTMy+b16HIXKgPqVXd/RN/+uHOEw==}
engines: {node: '>=18'}
peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc
zod: ^3.23.8
zod: ^3.25.76 || ^4
peerDependenciesMeta:
zod:
optional: true
'@ai-sdk/ui-utils@1.2.11':
resolution: {integrity: sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.23.8
'@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
@@ -1915,6 +1949,9 @@ packages:
'@shikijs/core@3.4.2':
resolution: {integrity: sha512-AG8vnSi1W2pbgR2B911EfGqtLE9c4hQBYkv/x7Z+Kt0VxhgQKcW7UNDVYsu9YxwV6u+OJrvdJrMq6DNWoBjihQ==}
'@shikijs/core@3.9.2':
resolution: {integrity: sha512-3q/mzmw09B2B6PgFNeiaN8pkNOixWS726IHmJEpjDAcneDPMQmUg2cweT9cWXY4XcyQS3i6mOOUgQz9RRUP6HA==}
'@shikijs/engine-javascript@3.4.2':
resolution: {integrity: sha512-1/adJbSMBOkpScCE/SB6XkjJU17ANln3Wky7lOmrnpl+zBdQ1qXUJg2GXTYVHRq+2j3hd1DesmElTXYDgtfSOQ==}
@@ -1927,9 +1964,15 @@ packages:
'@shikijs/themes@3.4.2':
resolution: {integrity: sha512-qAEuAQh+brd8Jyej2UDDf+b4V2g1Rm8aBIdvt32XhDPrHvDkEnpb7Kzc9hSuHUxz0Iuflmq7elaDuQAP9bHIhg==}
'@shikijs/transformers@3.9.2':
resolution: {integrity: sha512-MW5hT4TyUp6bNAgTExRYLk1NNasVQMTCw1kgbxHcEC0O5cbepPWaB+1k+JzW9r3SP2/R8kiens8/3E6hGKfgsA==}
'@shikijs/types@3.4.2':
resolution: {integrity: sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg==}
'@shikijs/types@3.9.2':
resolution: {integrity: sha512-/M5L0Uc2ljyn2jKvj4Yiah7ow/W+DJSglVafvWAJ/b8AZDeeRAdMu3c2riDzB7N42VD+jSnWxeP9AKtd4TfYVw==}
'@shikijs/vscode-textmate@10.0.2':
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
@@ -1938,6 +1981,9 @@ packages:
engines: {node: '>= 8.0.0'}
hasBin: true
'@standard-schema/spec@1.0.0':
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
'@swc/helpers@0.5.17':
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
@@ -2223,12 +2269,12 @@ packages:
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
'@types/diff-match-patch@1.0.36':
resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
'@types/dom-to-image@2.6.7':
resolution: {integrity: sha512-me5VbCv+fcXozblWwG13krNBvuEOm6kA5xoa4RrjDJCNFOZSWR3/QLtOXimBHk1Fisq69Gx3JtOoXtg1N1tijg==}
'@types/estree-jsx@1.0.5':
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
'@types/estree@1.0.7':
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
@@ -2241,6 +2287,9 @@ packages:
'@types/js-cookie@3.0.6':
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
'@types/katex@0.16.7':
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
'@types/linkify-it@3.0.5':
resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==}
@@ -2309,6 +2358,9 @@ packages:
'@types/turndown@5.0.5':
resolution: {integrity: sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w==}
'@types/unist@2.0.11':
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
'@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
@@ -2346,15 +2398,11 @@ packages:
resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
engines: {node: '>= 8.0.0'}
ai@4.3.16:
resolution: {integrity: sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g==}
ai@5.0.0-beta.34:
resolution: {integrity: sha512-AFJ4p35AxA+1KFtnoouePLaAUpoj0IxIAoq/xgIv88qzYajTg4Sac5KaV4CDHFRLoF0L2cwhlFXt/Ss/zyBKkA==}
engines: {node: '>=18'}
peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc
zod: ^3.23.8
peerDependenciesMeta:
react:
optional: true
zod: ^3.25.76 || ^4
ansi-align@3.0.1:
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
@@ -2506,6 +2554,9 @@ packages:
character-entities@2.0.2:
resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
character-reference-invalid@2.0.1:
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
@@ -2565,6 +2616,10 @@ packages:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
commander@8.3.0:
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
engines: {node: '>= 12'}
common-ancestor-path@1.0.1:
resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==}
@@ -2734,9 +2789,6 @@ packages:
dfa@1.2.0:
resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==}
diff-match-patch@1.0.5:
resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
diff@5.2.0:
resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==}
engines: {node: '>=0.3.1'}
@@ -2864,6 +2916,9 @@ packages:
engines: {node: '>=4'}
hasBin: true
estree-util-is-identifier-name@3.0.0:
resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==}
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
@@ -2881,6 +2936,10 @@ packages:
eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
eventsource-parser@3.0.3:
resolution: {integrity: sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==}
engines: {node: '>=20.0.0'}
extend-shallow@2.0.1:
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
engines: {node: '>=0.10.0'}
@@ -3052,6 +3111,12 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
hast-util-from-dom@5.0.1:
resolution: {integrity: sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==}
hast-util-from-html-isomorphic@2.0.0:
resolution: {integrity: sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==}
hast-util-from-html@2.0.3:
resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==}
@@ -3070,6 +3135,9 @@ packages:
hast-util-to-html@9.0.5:
resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
hast-util-to-jsx-runtime@2.3.6:
resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
hast-util-to-parse5@8.0.0:
resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==}
@@ -3096,6 +3164,9 @@ packages:
html-escaper@3.0.3:
resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==}
html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
@@ -3127,6 +3198,9 @@ packages:
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
inline-style-parser@0.2.4:
resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==}
iron-webcrypto@1.2.1:
resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==}
@@ -3134,9 +3208,18 @@ packages:
resolution: {integrity: sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
is-alphabetical@2.0.1:
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
is-alphanumerical@2.0.1:
resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==}
is-arrayish@0.3.2:
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
is-decimal@2.0.1:
resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
is-docker@3.0.0:
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -3158,6 +3241,9 @@ packages:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-hexadecimal@2.0.1:
resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
is-inside-container@1.0.0:
resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
engines: {node: '>=14.16'}
@@ -3224,14 +3310,13 @@ packages:
engines: {node: '>=6'}
hasBin: true
jsondiffpatch@0.6.0:
resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
katex@0.16.22:
resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==}
hasBin: true
kind-of@6.0.3:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
@@ -3486,6 +3571,18 @@ packages:
mdast-util-gfm@3.1.0:
resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==}
mdast-util-math@3.0.0:
resolution: {integrity: sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==}
mdast-util-mdx-expression@2.0.1:
resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==}
mdast-util-mdx-jsx@3.2.0:
resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==}
mdast-util-mdxjs-esm@2.0.1:
resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==}
mdast-util-phrasing@4.1.0:
resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
@@ -3535,6 +3632,9 @@ packages:
micromark-extension-gfm@3.0.0:
resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==}
micromark-extension-math@3.1.0:
resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==}
micromark-factory-destination@2.0.1:
resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==}
@@ -3776,6 +3876,9 @@ packages:
parse-css-color@0.2.1:
resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==}
parse-entities@4.0.2:
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
parse-latin@7.0.0:
resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==}
@@ -4065,6 +4168,12 @@ packages:
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
react-markdown@10.1.0:
resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==}
peerDependencies:
'@types/react': '>=18'
react: '>=18'
react-refresh@0.17.0:
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
engines: {node: '>=0.10.0'}
@@ -4137,6 +4246,9 @@ packages:
rehype-external-links@3.0.0:
resolution: {integrity: sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==}
rehype-katex@7.0.1:
resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==}
rehype-parse@9.0.1:
resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==}
@@ -4152,6 +4264,9 @@ packages:
remark-gfm@4.0.1:
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
remark-math@6.0.0:
resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==}
remark-parse@11.0.0:
resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==}
@@ -4354,6 +4469,12 @@ packages:
resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==}
engines: {node: '>=0.10.0'}
style-to-js@1.1.17:
resolution: {integrity: sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==}
style-to-object@1.0.9:
resolution: {integrity: sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==}
sucrase@3.35.0:
resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -4362,8 +4483,8 @@ packages:
suf-log@2.5.3:
resolution: {integrity: sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==}
swr@2.3.3:
resolution: {integrity: sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==}
swr@2.3.5:
resolution: {integrity: sha512-4e7pjTVulZTIL+b/S0RYFsgDcTcXPLUOvBPqyh9YdD+PkHeEMoaPwDmF9Kv6I1nnPg1OFKhiiEYpsYaaE2W2jA==}
peerDependencies:
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
@@ -4830,6 +4951,9 @@ packages:
zod@3.24.4:
resolution: {integrity: sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==}
zod@4.0.17:
resolution: {integrity: sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==}
zustand@4.5.6:
resolution: {integrity: sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==}
engines: {node: '>=12.7.0'}
@@ -4868,39 +4992,50 @@ packages:
snapshots:
'@ai-sdk/google@1.2.18(zod@3.24.4)':
'@ai-sdk/gateway@1.0.0-beta.19(zod@4.0.17)':
dependencies:
'@ai-sdk/provider': 2.0.0-beta.2
'@ai-sdk/provider-utils': 3.0.0-beta.10(zod@4.0.17)
zod: 4.0.17
'@ai-sdk/google@1.2.18(zod@4.0.17)':
dependencies:
'@ai-sdk/provider': 1.1.3
'@ai-sdk/provider-utils': 2.2.8(zod@3.24.4)
zod: 3.24.4
'@ai-sdk/provider-utils': 2.2.8(zod@4.0.17)
zod: 4.0.17
'@ai-sdk/provider-utils@2.2.8(zod@3.24.4)':
'@ai-sdk/provider-utils@2.2.8(zod@4.0.17)':
dependencies:
'@ai-sdk/provider': 1.1.3
nanoid: 3.3.11
secure-json-parse: 2.7.0
zod: 3.24.4
zod: 4.0.17
'@ai-sdk/provider-utils@3.0.0-beta.10(zod@4.0.17)':
dependencies:
'@ai-sdk/provider': 2.0.0-beta.2
'@standard-schema/spec': 1.0.0
eventsource-parser: 3.0.3
zod: 4.0.17
zod-to-json-schema: 3.24.5(zod@4.0.17)
'@ai-sdk/provider@1.1.3':
dependencies:
json-schema: 0.4.0
'@ai-sdk/react@1.2.12(react@19.1.0)(zod@3.24.4)':
'@ai-sdk/provider@2.0.0-beta.2':
dependencies:
'@ai-sdk/provider-utils': 2.2.8(zod@3.24.4)
'@ai-sdk/ui-utils': 1.2.11(zod@3.24.4)
json-schema: 0.4.0
'@ai-sdk/react@2.0.0-beta.34(react@19.1.0)(zod@4.0.17)':
dependencies:
'@ai-sdk/provider-utils': 3.0.0-beta.10(zod@4.0.17)
ai: 5.0.0-beta.34(zod@4.0.17)
react: 19.1.0
swr: 2.3.3(react@19.1.0)
swr: 2.3.5(react@19.1.0)
throttleit: 2.1.0
optionalDependencies:
zod: 3.24.4
'@ai-sdk/ui-utils@1.2.11(zod@3.24.4)':
dependencies:
'@ai-sdk/provider': 1.1.3
'@ai-sdk/provider-utils': 2.2.8(zod@3.24.4)
zod: 3.24.4
zod-to-json-schema: 3.24.5(zod@3.24.4)
zod: 4.0.17
'@alloc/quick-lru@5.2.0': {}
@@ -6377,6 +6512,13 @@ snapshots:
'@types/hast': 3.0.4
hast-util-to-html: 9.0.5
'@shikijs/core@3.9.2':
dependencies:
'@shikijs/types': 3.9.2
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
hast-util-to-html: 9.0.5
'@shikijs/engine-javascript@3.4.2':
dependencies:
'@shikijs/types': 3.4.2
@@ -6396,11 +6538,21 @@ snapshots:
dependencies:
'@shikijs/types': 3.4.2
'@shikijs/transformers@3.9.2':
dependencies:
'@shikijs/core': 3.9.2
'@shikijs/types': 3.9.2
'@shikijs/types@3.4.2':
dependencies:
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
'@shikijs/types@3.9.2':
dependencies:
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
'@shikijs/vscode-textmate@10.0.2': {}
'@shuding/opentype.js@1.4.0-beta.0':
@@ -6408,6 +6560,8 @@ snapshots:
fflate: 0.7.4
string.prototype.codepointat: 0.2.1
'@standard-schema/spec@1.0.0': {}
'@swc/helpers@0.5.17':
dependencies:
tslib: 2.8.1
@@ -6686,10 +6840,12 @@ snapshots:
dependencies:
'@types/ms': 2.1.0
'@types/diff-match-patch@1.0.36': {}
'@types/dom-to-image@2.6.7': {}
'@types/estree-jsx@1.0.5':
dependencies:
'@types/estree': 1.0.7
'@types/estree@1.0.7': {}
'@types/fontkit@2.0.8':
@@ -6702,6 +6858,8 @@ snapshots:
'@types/js-cookie@3.0.6': {}
'@types/katex@0.16.7': {}
'@types/linkify-it@3.0.5': {}
'@types/linkify-it@5.0.0': {}
@@ -6775,6 +6933,8 @@ snapshots:
'@types/turndown@5.0.5': {}
'@types/unist@2.0.11': {}
'@types/unist@3.0.3': {}
'@types/use-sync-external-store@0.0.6': {}
@@ -6823,17 +6983,13 @@ snapshots:
dependencies:
humanize-ms: 1.2.1
ai@4.3.16(react@19.1.0)(zod@3.24.4):
ai@5.0.0-beta.34(zod@4.0.17):
dependencies:
'@ai-sdk/provider': 1.1.3
'@ai-sdk/provider-utils': 2.2.8(zod@3.24.4)
'@ai-sdk/react': 1.2.12(react@19.1.0)(zod@3.24.4)
'@ai-sdk/ui-utils': 1.2.11(zod@3.24.4)
'@ai-sdk/gateway': 1.0.0-beta.19(zod@4.0.17)
'@ai-sdk/provider': 2.0.0-beta.2
'@ai-sdk/provider-utils': 3.0.0-beta.10(zod@4.0.17)
'@opentelemetry/api': 1.9.0
jsondiffpatch: 0.6.0
zod: 3.24.4
optionalDependencies:
react: 19.1.0
zod: 4.0.17
ansi-align@3.0.1:
dependencies:
@@ -7053,6 +7209,8 @@ snapshots:
character-entities@2.0.2: {}
character-reference-invalid@2.0.1: {}
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
@@ -7097,6 +7255,8 @@ snapshots:
commander@4.1.1: {}
commander@8.3.0: {}
common-ancestor-path@1.0.1: {}
commondir@1.0.1: {}
@@ -7236,8 +7396,6 @@ snapshots:
dfa@1.2.0: {}
diff-match-patch@1.0.5: {}
diff@5.2.0: {}
dir-glob@3.0.1:
@@ -7360,6 +7518,8 @@ snapshots:
esprima@4.0.1: {}
estree-util-is-identifier-name@3.0.0: {}
estree-walker@2.0.2: {}
estree-walker@3.0.3:
@@ -7372,6 +7532,8 @@ snapshots:
eventemitter3@5.0.1: {}
eventsource-parser@3.0.3: {}
extend-shallow@2.0.1:
dependencies:
is-extendable: 0.1.1
@@ -7576,6 +7738,19 @@ snapshots:
dependencies:
function-bind: 1.1.2
hast-util-from-dom@5.0.1:
dependencies:
'@types/hast': 3.0.4
hastscript: 9.0.1
web-namespaces: 2.0.1
hast-util-from-html-isomorphic@2.0.0:
dependencies:
'@types/hast': 3.0.4
hast-util-from-dom: 5.0.1
hast-util-from-html: 2.0.3
unist-util-remove-position: 5.0.0
hast-util-from-html@2.0.3:
dependencies:
'@types/hast': 3.0.4
@@ -7634,6 +7809,26 @@ snapshots:
stringify-entities: 4.0.4
zwitch: 2.0.4
hast-util-to-jsx-runtime@2.3.6:
dependencies:
'@types/estree': 1.0.7
'@types/hast': 3.0.4
'@types/unist': 3.0.3
comma-separated-tokens: 2.0.3
devlop: 1.1.0
estree-util-is-identifier-name: 3.0.0
hast-util-whitespace: 3.0.0
mdast-util-mdx-expression: 2.0.1
mdast-util-mdx-jsx: 3.2.0
mdast-util-mdxjs-esm: 2.0.1
property-information: 7.1.0
space-separated-tokens: 2.0.2
style-to-js: 1.1.17
unist-util-position: 5.0.0
vfile-message: 4.0.2
transitivePeerDependencies:
- supports-color
hast-util-to-parse5@8.0.0:
dependencies:
'@types/hast': 3.0.4
@@ -7671,6 +7866,8 @@ snapshots:
html-escaper@3.0.3: {}
html-url-attributes@3.0.1: {}
html-void-elements@3.0.0: {}
htmlparser2@8.0.2:
@@ -7702,12 +7899,23 @@ snapshots:
inherits@2.0.4: {}
inline-style-parser@0.2.4: {}
iron-webcrypto@1.2.1: {}
is-absolute-url@4.0.1: {}
is-alphabetical@2.0.1: {}
is-alphanumerical@2.0.1:
dependencies:
is-alphabetical: 2.0.1
is-decimal: 2.0.1
is-arrayish@0.3.2: {}
is-decimal@2.0.1: {}
is-docker@3.0.0: {}
is-extendable@0.1.1: {}
@@ -7720,6 +7928,8 @@ snapshots:
dependencies:
is-extglob: 2.1.1
is-hexadecimal@2.0.1: {}
is-inside-container@1.0.0:
dependencies:
is-docker: 3.0.0
@@ -7767,18 +7977,16 @@ snapshots:
json5@2.2.3: {}
jsondiffpatch@0.6.0:
dependencies:
'@types/diff-match-patch': 1.0.36
chalk: 5.4.1
diff-match-patch: 1.0.5
jsonfile@6.1.0:
dependencies:
universalify: 2.0.1
optionalDependencies:
graceful-fs: 4.2.11
katex@0.16.22:
dependencies:
commander: 8.3.0
kind-of@6.0.3: {}
kleur@3.0.3: {}
@@ -8045,6 +8253,57 @@ snapshots:
transitivePeerDependencies:
- supports-color
mdast-util-math@3.0.0:
dependencies:
'@types/hast': 3.0.4
'@types/mdast': 4.0.4
devlop: 1.1.0
longest-streak: 3.1.0
mdast-util-from-markdown: 2.0.2
mdast-util-to-markdown: 2.1.2
unist-util-remove-position: 5.0.0
transitivePeerDependencies:
- supports-color
mdast-util-mdx-expression@2.0.1:
dependencies:
'@types/estree-jsx': 1.0.5
'@types/hast': 3.0.4
'@types/mdast': 4.0.4
devlop: 1.1.0
mdast-util-from-markdown: 2.0.2
mdast-util-to-markdown: 2.1.2
transitivePeerDependencies:
- supports-color
mdast-util-mdx-jsx@3.2.0:
dependencies:
'@types/estree-jsx': 1.0.5
'@types/hast': 3.0.4
'@types/mdast': 4.0.4
'@types/unist': 3.0.3
ccount: 2.0.1
devlop: 1.1.0
mdast-util-from-markdown: 2.0.2
mdast-util-to-markdown: 2.1.2
parse-entities: 4.0.2
stringify-entities: 4.0.4
unist-util-stringify-position: 4.0.0
vfile-message: 4.0.2
transitivePeerDependencies:
- supports-color
mdast-util-mdxjs-esm@2.0.1:
dependencies:
'@types/estree-jsx': 1.0.5
'@types/hast': 3.0.4
'@types/mdast': 4.0.4
devlop: 1.1.0
mdast-util-from-markdown: 2.0.2
mdast-util-to-markdown: 2.1.2
transitivePeerDependencies:
- supports-color
mdast-util-phrasing@4.1.0:
dependencies:
'@types/mdast': 4.0.4
@@ -8163,6 +8422,16 @@ snapshots:
micromark-util-combine-extensions: 2.0.1
micromark-util-types: 2.0.2
micromark-extension-math@3.1.0:
dependencies:
'@types/katex': 0.16.7
devlop: 1.1.0
katex: 0.16.22
micromark-factory-space: 2.0.1
micromark-util-character: 2.1.1
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.2
micromark-factory-destination@2.0.1:
dependencies:
micromark-util-character: 2.1.1
@@ -8377,7 +8646,7 @@ snapshots:
regex: 6.0.1
regex-recursion: 6.0.2
openai@4.100.0(zod@3.24.4):
openai@4.100.0(zod@4.0.17):
dependencies:
'@types/node': 18.19.100
'@types/node-fetch': 2.6.12
@@ -8387,7 +8656,7 @@ snapshots:
formdata-node: 4.4.1
node-fetch: 2.7.0
optionalDependencies:
zod: 3.24.4
zod: 4.0.17
transitivePeerDependencies:
- encoding
@@ -8425,6 +8694,16 @@ snapshots:
color-name: 1.1.4
hex-rgb: 4.3.0
parse-entities@4.0.2:
dependencies:
'@types/unist': 2.0.11
character-entities-legacy: 3.0.0
character-reference-invalid: 2.0.1
decode-named-character-reference: 1.1.0
is-alphanumerical: 2.0.1
is-decimal: 2.0.1
is-hexadecimal: 2.0.1
parse-latin@7.0.0:
dependencies:
'@types/nlcst': 2.0.3
@@ -8736,6 +9015,24 @@ snapshots:
react-is@16.13.1: {}
react-markdown@10.1.0(@types/react@19.1.4)(react@19.1.0):
dependencies:
'@types/hast': 3.0.4
'@types/mdast': 4.0.4
'@types/react': 19.1.4
devlop: 1.1.0
hast-util-to-jsx-runtime: 2.3.6
html-url-attributes: 3.0.1
mdast-util-to-hast: 13.2.0
react: 19.1.0
remark-parse: 11.0.0
remark-rehype: 11.1.2
unified: 11.0.5
unist-util-visit: 5.0.0
vfile: 6.0.3
transitivePeerDependencies:
- supports-color
react-refresh@0.17.0: {}
react-remove-scroll-bar@2.3.8(@types/react@19.1.4)(react@19.1.0):
@@ -8809,6 +9106,16 @@ snapshots:
space-separated-tokens: 2.0.2
unist-util-visit: 5.0.0
rehype-katex@7.0.1:
dependencies:
'@types/hast': 3.0.4
'@types/katex': 0.16.7
hast-util-from-html-isomorphic: 2.0.0
hast-util-to-text: 4.0.2
katex: 0.16.22
unist-util-visit-parents: 6.0.1
vfile: 6.0.3
rehype-parse@9.0.1:
dependencies:
'@types/hast': 3.0.4
@@ -8845,6 +9152,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
remark-math@6.0.0:
dependencies:
'@types/mdast': 4.0.4
mdast-util-math: 3.0.0
micromark-extension-math: 3.1.0
unified: 11.0.5
transitivePeerDependencies:
- supports-color
remark-parse@11.0.0:
dependencies:
'@types/mdast': 4.0.4
@@ -9155,6 +9471,14 @@ snapshots:
dependencies:
escape-string-regexp: 1.0.5
style-to-js@1.1.17:
dependencies:
style-to-object: 1.0.9
style-to-object@1.0.9:
dependencies:
inline-style-parser: 0.2.4
sucrase@3.35.0:
dependencies:
'@jridgewell/gen-mapping': 0.3.8
@@ -9169,7 +9493,7 @@ snapshots:
dependencies:
s.color: 0.0.15
swr@2.3.3(react@19.1.0):
swr@2.3.5(react@19.1.0):
dependencies:
dequal: 2.0.3
react: 19.1.0
@@ -9547,6 +9871,10 @@ snapshots:
dependencies:
zod: 3.24.4
zod-to-json-schema@3.24.5(zod@4.0.17):
dependencies:
zod: 4.0.17
zod-to-ts@1.2.0(typescript@5.8.3)(zod@3.24.4):
dependencies:
typescript: 5.8.3
@@ -9554,6 +9882,8 @@ snapshots:
zod@3.24.4: {}
zod@4.0.17: {}
zustand@4.5.6(@types/react@19.1.4)(react@19.1.0):
dependencies:
use-sync-external-store: 1.5.0(react@19.1.0)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 431 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 572 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 756 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 685 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 835 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 602 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 516 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1113,8 +1113,19 @@
},
"dLEg4IA3F5jgc44Bst9if": {
"title": "Models on Hugging Face",
"description": "",
"links": []
"description": "Embedding models are used to convert raw data like text, code, or images into high-dimensional vectors that capture semantic meaning. These vector representations allow AI systems to compare, cluster, and retrieve information based on similarity rather than exact matches. Hugging Face provides a wide range of pretrained embedding models such as `all-MiniLM-L6-v2`, `gte-base`, `Qwen3-Embedding-8B` and `bge-base` which are commonly used for tasks like semantic search, recommendation systems, duplicate detection, and retrieval-augmented generation (RAG). These models can be accessed through libraries like transformers or sentence-transformers, making it easy to generate high-quality embeddings for both general-purpose and task-specific applications.\n\nLearn more from the following resources:",
"links": [
{
"title": "Hugging Face Embedding Models",
"url": "https://huggingface.co/models?pipeline_tag=feature-extraction",
"type": "article"
},
{
"title": "Hugging Face - Text embeddings & semantic search",
"url": "https://www.youtube.com/watch?v=OATCgQtNX2o",
"type": "video"
}
]
},
"tt9u3oFlsjEMfPyojuqpc": {
"title": "Vector Databases",

View File

@@ -137,6 +137,11 @@
"title": "Anatomy of a Component",
"url": "https://angular.dev/guide/components",
"type": "article"
},
{
"title": "Anatomy of a Component - Interactive Tutorial",
"url": "https://angular.dev/tutorials/learn-angular/1-components-in-angular",
"type": "article"
}
]
},

View File

@@ -725,7 +725,7 @@
},
"2vQPmVNk1QpMM-15RKG8b": {
"title": "Metrics",
"description": "In Amazon CloudWatch, **metrics** are fundamental concepts that you work with. A metric is the fundamental concept in CloudWatch and represents a time-ordered set of data points that are published to CloudWatch. Think of a metric as a variable to monitor, and the data points as representing the values of that variable over time. Metrics are uniquely defined by a name, a namespace, and zero or more dimensions up to 30 dimensions per metric. Every data point must have a timestamp. You can retrieve statistics about those data points as an ordered set of time-series data. CloudWatch provides metrics for every serviece in AWS.\n\nLearn more from the following resources:",
"description": "In Amazon CloudWatch, **metrics** are fundamental concepts that you work with. A metric is the fundamental concept in CloudWatch and represents a time-ordered set of data points that are published to CloudWatch. Think of a metric as a variable to monitor, and the data points as representing the values of that variable over time. Metrics are uniquely defined by a name, a namespace, and zero or more dimensions up to 30 dimensions per metric. Every data point must have a timestamp. You can retrieve statistics about those data points as an ordered set of time-series data. CloudWatch provides metrics for every service in AWS.\n\nLearn more from the following resources:",
"links": [
{
"title": "CloudWatch Metrics",

View File

@@ -2833,11 +2833,6 @@
"url": "https://chain.link/education-hub/blockchain",
"type": "article"
},
{
"title": "What Is The Blockchain And Why Does It Matter?",
"url": "https://www.forbes.com/sites/theyec/2020/05/18/what-is-the-blockchain-and-why-does-it-matter/",
"type": "article"
},
{
"title": "Web3/Crypto: Why Bother?",
"url": "https://continuations.com/post/671863718643105792/web3crypto-why-bother",

View File

@@ -4,7 +4,7 @@
"description": "Fundamental IT skills form the backbone of cybersecurity proficiency and encompass a broad range of technical knowledge. These skills include understanding computer hardware and software, networking concepts, and operating systems (particularly Windows and Linux). Proficiency in at least one programming language, such as Python or JavaScript, is increasingly important for automation and scripting tasks. Database management, including SQL, is crucial for handling and securing data. Knowledge of cloud computing platforms like AWS or Azure is becoming essential as organizations migrate to cloud environments. Familiarity with basic cybersecurity concepts such as encryption, access control, and common attack vectors provides a foundation for more advanced security work. Additionally, troubleshooting skills, the ability to interpret logs, and a basic understanding of web technologies are vital. These fundamental IT skills enable cybersecurity professionals to effectively protect systems, identify vulnerabilities, and respond to incidents in increasingly complex technological landscapes.\n\nLearn more from the following resources:",
"links": [
{
"title": "7 In-Demand IT Skills to Boost Your Resume in 2025",
"title": "8 In-Demand IT Skills to Boost Your Resume in 2025",
"url": "https://www.coursera.org/articles/key-it-skills-for-your-career",
"type": "article"
},
@@ -342,6 +342,11 @@
"url": "https://www.comptia.org/certifications/linux",
"type": "article"
},
{
"title": "CompTIA Linux+ Certification Training Labs",
"url": "https://github.com/labex-labs/comptia-linux-plus-training-labs",
"type": "article"
},
{
"title": "Linux+ Exam Prep",
"url": "https://www.youtube.com/watch?v=niPWk7tgD2Q&list=PL78ppT-_wOmuwT9idLvuoKOn6UYurFKCp",

View File

@@ -246,7 +246,12 @@
"links": [
{
"title": "Design Systems: Pilots & Scorecards",
"url": "https://superfriendly.com/design-systems/articles/design-systems-pilots-scorecards/",
"url": "https://danmall.com/posts/design-systems-pilots-scorecards/",
"type": "article"
},
{
"title": "How to run a design system pilot",
"url": "https://university.obvious.in/product-design/design-system/how-to-run-a-design-system-pilot",
"type": "article"
}
]

View File

@@ -570,8 +570,8 @@
"type": "article"
},
{
"title": "Vim Adventures",
"url": "https://vim-adventures.com/",
"title": "Practical Vim 2nd Edition",
"url": "https://dokumen.pub/practical-vim-2nd-edition-2nd-edition-9781680501278.html",
"type": "article"
},
{

View File

@@ -153,6 +153,11 @@
"title": "Underlying Technologies - Medium",
"url": "https://medium.com/@furkan.turkal/how-does-docker-actually-work-the-hard-way-a-technical-deep-diving-c5b8ea2f0422",
"type": "article"
},
{
"title": "Containers - Namespaces, Cgroups and Overlay Filesystem",
"url": "https://www.youtube.com/watch?v=wJdDWc6zO4U",
"type": "video"
}
]
},
@@ -378,22 +383,6 @@
}
]
},
"HlTxLqKNFMhghtKF6AcWu": {
"title": "Interactive Test Environments",
"description": "Docker allows you to create isolated, disposable environments that can be deleted once you're done with testing. This makes it much easier to work with third party software, test different dependencies or versions, and quickly experiment without the risk of damaging your local setup.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Launch a Dev Environment",
"url": "https://docs.docker.com/desktop/dev-environments/create-dev-env/",
"type": "article"
},
{
"title": "Test Environments - Medium",
"url": "https://manishsaini74.medium.com/containerized-testing-orchestrating-test-environments-with-docker-5201bfadfdf2",
"type": "article"
}
]
},
"YzpB7rgSR4ueQRLa0bRWa": {
"title": "Command Line Utilities",
"description": "Docker images can include command line utilities or standalone applications that we can run inside containers.\n\nVisit the following resources to learn more:",

View File

@@ -568,11 +568,6 @@
"description": "The role of an Engineering Manager extends to external collaboration as well. Here, they often serve the role of liaising with external teams, vendors, or partners, aligning goals and ensuring smooth communication flow. The key responsibilities include managing relationships, understanding the partner ecosystem, and negotiating win-win situations.\n\nEngineering Managers face challenges like cultural differences, communication hurdles, or time zone disparities. They address these by building reliability through regular updates, clear agendas, and understanding each other's work culture.\n\nTo succeed, Engineering Managers need good interpersonal skills, a keen eye for future opportunities, and the ability to adapt quickly. An understanding of business and sales, alongside engineering knowledge, can be advantageous too. This role needs balance - drive details when necessary and step back and delegate when appropriate.",
"links": []
},
"TQY4hjo56rDdlbzjs_-nl": {
"title": "Competitive Analysis",
"description": "An Engineering Manager uses competitive analysis to understand market trends and competitor strategies. This aids in decision-making and strategic planning. Their key responsibilities include identifying key competitors, analyzing their products, sales, and marketing strategies.\n\nChallenges may arise from having incomplete or inaccurate data. In these cases, Engineering Managers have to rely on their judgement and experience. Their analysis should be unbiased and as accurate as possible to influence the right design and development strategies.\n\nSuccessful competitive analysis requires strong analytical skills, keen attention to detail, and the ability to understand complex market dynamics. Managers must stay updated on market trend, technological advancements and be able to distinguish their company's unique selling proposition. This will allow them to plan steps to maintain competitiveness in the market.",
"links": []
},
"QUxpEK8smXRBs2gMdDInB": {
"title": "Legacy System Retirement",
"description": "Every Engineering Manager knows the value and hurdles of legacy system retirement. They must plan and manage this complex task with a keen understanding of the system's purpose, its interdependencies, and potential risks of its retirement. Key responsibilities include assessing the impact on users, mitigating downtime, and ensuring business continuity.\n\nChallenges often arise from lack of documentation or knowledge about the legacy system. To overcome this, they could organize knowledge-sharing sessions with long-standing team members, assessing external help, or gradual transition methods.\n\nThe successful retirement of a legacy system requires a comprehensive approach, good interpersonal skills for team collaboration, and strong decision-making skills. An Engineering Manager has to balance the systems business value against the cost and risk of maintaining it.",

View File

@@ -448,7 +448,7 @@
},
"SHTSvMDqI7X1_ZT7-m--n": {
"title": "Linux Basics",
"description": "Knowledge of UNIX is a must for almost all kind of development as most of the codes that you write is most likely going to be finally deployed on a UNIX/Linux machine. Linux has been the backbone of the free and open source software movement, providing a simple and elegant operating system for almost all your needs.\n\nVisit the following resources to learn more:",
"description": "Knowledge of UNIX is a must for almost all kind of development as most of the code that you write is most likely going to be finally deployed on a UNIX/Linux machine. Linux has been the backbone of the free and open source software movement, providing a simple and elegant operating system for almost all your needs.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Coursera - Unix Courses",

File diff suppressed because it is too large Load Diff

View File

@@ -305,11 +305,6 @@
"title": "Method Chaining",
"description": "Method chaining is a programming technique where multiple method calls are made sequentially on the same object, one after another, in a single statement. Each method in the chain returns an object, allowing the next method to be called on that returned object. This approach enhances code readability and conciseness by reducing the need for temporary variables and intermediate steps.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Java Method Chaining - Java Explained",
"url": "https://bito.ai/resources/java-method-chaining-java-explained",
"type": "article"
},
{
"title": "How to achieve method chaining in Java",
"url": "https://stackoverflow.com/questions/21180269/how-to-achieve-method-chaining-in-java",

View File

@@ -1250,11 +1250,6 @@
"title": "Backend Automation",
"description": "Backend Testing is a testing method that checks the server side or database of web applications or software. Backend testing aims to test the application layer or database layer to ensure that the web application or software is free from database defects like deadlock, data corruption, or data loss.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "What is Backend Testing?",
"url": "https://testinggenez.com/what-is-backend-testing-and-types/",
"type": "article"
},
{
"title": "Backend Testing Tutorial",
"url": "https://www.guru99.com/what-is-backend-testing.html",

View File

@@ -276,19 +276,8 @@
},
"9naCfoHF1LW1OEsVZGi8v": {
"title": "Keep it simple and refactor often",
"description": "Keeping framework code distant refers to separating the application's code from the framework's code. By doing so, it makes it easier to maintain, test, and upgrade the application's codebase and the framework independently.\n\nHere are some ways to keep framework code distant in system architecture:\n\n1. Use an abstraction layer to separate the application code from the framework code. This allows the application code to be written without the need to know the specifics of the framework.\n2. Use dependency injection to decouple the application code from the framework code. This allows the application code to use the framework's functionality without having to instantiate the framework objects directly.\n3. Avoid using framework-specific libraries or classes in the application code. This makes it easier to switch to a different framework in the future if needed.\n4. Use a standard interface for the application code to interact with the framework. This allows the application code to be written without the need to know the specifics of the framework.\n5. Keep the application and the framework code in separate projects and/or repositories.\n\nBy following these best practices, the system architecture will be more maintainable, testable, and less error-prone, and it will be easier to upgrade or switch the framework if needed.\n\nLearn more from the following links:",
"links": [
{
"title": "Clean architecture",
"url": "https://pusher.com/tutorials/clean-architecture-introduction/",
"type": "article"
},
{
"title": "Explore top posts about General Programming",
"url": "https://app.daily.dev/tags/general-programming?ref=roadmapsh",
"type": "article"
}
]
"description": "",
"links": []
},
"TDhTYdEyBuOnDKcQJzTAk": {
"title": "Programming Paradigms",

View File

@@ -107,7 +107,7 @@
"links": [
{
"title": "SQL Operators: 6 Different Types",
"url": "https://www.dataquest.io/blog/sql-operators/",
"url": "https://dataengineeracademy.com/blog/sql-operators-6-different-types-code-examples/",
"type": "article"
}
]

View File

@@ -1321,16 +1321,10 @@
}
]
},
"LncTxPg-wx8loy55r5NmV": {
"title": "Queue-Based Load Leveling",
"description": "Use a queue that acts as a buffer between a task and a service it invokes in order to smooth intermittent heavy loads that can cause the service to fail or the task to time out. This can help to minimize the impact of peaks in demand on availability and responsiveness for both the task and the service.\n\nTo learn more visit the following links:",
"links": [
{
"title": "Queue-Based Load Leveling pattern",
"url": "https://learn.microsoft.com/en-us/azure/architecture/patterns/queue-based-load-leveling",
"type": "article"
}
]
"queu-based-load-leveling@LncTxPg-wx8loy55r5NmV.md": {
"title": "Queu-based Load Leveling",
"description": "",
"links": []
},
"2ryzJhRDTo98gGgn9mAxR": {
"title": "Publisher/Subscriber",

View File

@@ -51,9 +51,9 @@
}
]
},
"j69erqfosSZMDlmKcnnn0": {
"title": "Role of Technical Writers in Organizations",
"description": "The role of a **Technical Writer** is primarily to translate complex technical information into simpler language that is easy to understand for a non-technical audience. They design, write, edit, and rewrite technical pieces like operating instructions, FAQs, installation guides, and more. Apart from this, they also gather and disseminate technical information among customers, designers, and manufacturers. Essentially, their job involves communicating technical terminologies and a clear understanding of complex information to those who need it in an easy-to-understand format.",
"role-of-technical-writers-inorganizations@j69erqfosSZMDlmKcnnn0.md": {
"title": "Role of Technical Writers inOrganizations",
"description": "",
"links": []
},
"cNeT1dJDfgn0ndPzSxhSL": {

View File

@@ -25,8 +25,8 @@
"type": "opensource"
},
{
"title": "Creating a Vue Project",
"url": "https://cli.vuejs.org/guide/creating-a-project.html",
"title": "Quick Start | Vue.js",
"url": "https://vuejs.org/guide/quick-start.html",
"type": "article"
},
{

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

BIN
public/roadmaps/nextjs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

View File

@@ -36,7 +36,7 @@ Here is the list of available roadmaps with more being actively worked upon.
- [Backend Roadmap](https://roadmap.sh/backend) / [Backend Beginner Roadmap](https://roadmap.sh/backend?r=backend-beginner)
- [DevOps Roadmap](https://roadmap.sh/devops) / [DevOps Beginner Roadmap](https://roadmap.sh/devops?r=devops-beginner)
- [Full Stack Roadmap](https://roadmap.sh/full-stack)
- [Git and GitHub](https://roadmap.sh/git-github)
- [Git and GitHub](https://roadmap.sh/git-github) / [Git and GitHub Beginner](https://roadmap.sh/git-github?r=git-github-beginner)
- [API Design Roadmap](https://roadmap.sh/api-design)
- [Computer Science Roadmap](https://roadmap.sh/computer-science)
- [Data Structures and Algorithms Roadmap](https://roadmap.sh/datastructures-and-algorithms)
@@ -47,6 +47,9 @@ Here is the list of available roadmaps with more being actively worked upon.
- [Linux Roadmap](https://roadmap.sh/linux)
- [Terraform Roadmap](https://roadmap.sh/terraform)
- [Data Analyst Roadmap](https://roadmap.sh/data-analyst)
- [BI Analyst Roadmap](https://roadmap.sh/bi-analyst)
- [Data Engineer Roadmap](https://roadmap.sh/data-engineer)
- [Machine Learning Roadmap](https://roadmap.sh/machine-learning)
- [MLOps Roadmap](https://roadmap.sh/mlops)
- [Product Manager Roadmap](https://roadmap.sh/product-manager)
- [Engineering Manager Roadmap](https://roadmap.sh/engineering-manager)
@@ -59,6 +62,7 @@ Here is the list of available roadmaps with more being actively worked upon.
- [TypeScript Roadmap](https://roadmap.sh/typescript)
- [C++ Roadmap](https://roadmap.sh/cpp)
- [React Roadmap](https://roadmap.sh/react)
- [Next.js Roadmap](https://roadmap.sh/nextjs)
- [React Native Roadmap](https://roadmap.sh/react-native)
- [Vue Roadmap](https://roadmap.sh/vue)
- [Angular Roadmap](https://roadmap.sh/angular)

View File

@@ -0,0 +1,255 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import type { OfficialRoadmapDocument } from '../src/queries/official-roadmap';
import { parse } from 'node-html-parser';
import { markdownToHtml } from '../src/lib/markdown';
import { htmlToMarkdown } from '../src/lib/html';
import matter from 'gray-matter';
import type { RoadmapFrontmatter } from '../src/lib/roadmap';
import {
allowedOfficialRoadmapTopicResourceType,
type AllowedOfficialRoadmapTopicResourceType,
type SyncToDatabaseTopicContent,
} from '../src/queries/official-roadmap-topic';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const args = process.argv.slice(2);
const secret = args
.find((arg) => arg.startsWith('--secret='))
?.replace('--secret=', '');
if (!secret) {
throw new Error('Secret is required');
}
let roadmapJsonCache: Map<string, OfficialRoadmapDocument> = new Map();
export async function fetchRoadmapJson(
roadmapId: string,
): Promise<OfficialRoadmapDocument> {
if (roadmapJsonCache.has(roadmapId)) {
return roadmapJsonCache.get(roadmapId)!;
}
const response = await fetch(
`https://roadmap.sh/api/v1-official-roadmap/${roadmapId}`,
);
if (!response.ok) {
throw new Error(
`Failed to fetch roadmap json: ${response.statusText} for ${roadmapId}`,
);
}
const data = await response.json();
if (data.error) {
throw new Error(
`Failed to fetch roadmap json: ${data.error} for ${roadmapId}`,
);
}
roadmapJsonCache.set(roadmapId, data);
return data;
}
export async function syncContentToDatabase(
topics: SyncToDatabaseTopicContent[],
) {
const response = await fetch(
`https://roadmap.sh/api/v1-sync-official-roadmap-topics`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
topics,
secret,
}),
},
);
if (!response.ok) {
const error = await response.json();
throw new Error(
`Failed to sync content to database: ${response.statusText} ${JSON.stringify(error, null, 2)}`,
);
}
return response.json();
}
// Directory containing the roadmaps
const ROADMAP_CONTENT_DIR = path.join(__dirname, '../src/data/roadmaps');
const allRoadmaps = await fs.readdir(ROADMAP_CONTENT_DIR);
const editorRoadmapIds = new Set<string>();
for (const roadmapId of allRoadmaps) {
const roadmapFrontmatterDir = path.join(
ROADMAP_CONTENT_DIR,
roadmapId,
`${roadmapId}.md`,
);
const roadmapFrontmatterRaw = await fs.readFile(
roadmapFrontmatterDir,
'utf-8',
);
const { data } = matter(roadmapFrontmatterRaw);
const roadmapFrontmatter = data as RoadmapFrontmatter;
if (roadmapFrontmatter.renderer === 'editor') {
editorRoadmapIds.add(roadmapId);
}
}
for (const roadmapId of editorRoadmapIds) {
try {
const roadmap = await fetchRoadmapJson(roadmapId);
const files = await fs.readdir(
path.join(ROADMAP_CONTENT_DIR, roadmapId, 'content'),
);
console.log(`🚀 Starting ${files.length} files for ${roadmapId}`);
const topics: SyncToDatabaseTopicContent[] = [];
for (const file of files) {
const isContentFile = file.endsWith('.md');
if (!isContentFile) {
console.log(`🚨 Skipping ${file} because it is not a content file`);
continue;
}
const nodeSlug = file.replace('.md', '');
if (!nodeSlug) {
console.error(`🚨 Node id is required: ${file}`);
continue;
}
const nodeId = nodeSlug.split('@')?.[1];
if (!nodeId) {
console.error(`🚨 Node id is required: ${file}`);
continue;
}
const node = roadmap.nodes.find((node) => node.id === nodeId);
if (!node) {
console.error(`🚨 Node not found: ${file}`);
continue;
}
const filePath = path.join(
ROADMAP_CONTENT_DIR,
roadmapId,
'content',
`${nodeSlug}.md`,
);
const fileExists = await fs
.stat(filePath)
.then(() => true)
.catch(() => false);
if (!fileExists) {
console.log(`🚨 File not found: ${filePath}`);
continue;
}
const content = await fs.readFile(filePath, 'utf8');
const html = markdownToHtml(content, false);
const rootHtml = parse(html);
let ulWithLinks: HTMLElement | undefined;
rootHtml.querySelectorAll('ul').forEach((ul) => {
const listWithJustLinks = Array.from(ul.querySelectorAll('li')).filter(
(li) => {
const link = li.querySelector('a');
return link && link.textContent?.trim() === li.textContent?.trim();
},
);
if (listWithJustLinks.length > 0) {
// @ts-expect-error - TODO: fix this
ulWithLinks = ul;
}
});
const listLinks: SyncToDatabaseTopicContent['resources'] =
ulWithLinks !== undefined
? Array.from(ulWithLinks.querySelectorAll('li > a'))
.map((link) => {
const typePattern = /@([a-z.]+)@/;
let linkText = link.textContent || '';
const linkHref = link.getAttribute('href') || '';
let linkType = linkText.match(typePattern)?.[1] || 'article';
linkType = allowedOfficialRoadmapTopicResourceType.includes(
linkType as any,
)
? linkType
: 'article';
linkText = linkText.replace(typePattern, '');
if (!linkText || !linkHref) {
return null;
}
return {
title: linkText,
url: linkHref,
type: linkType as AllowedOfficialRoadmapTopicResourceType,
};
})
.filter((link) => link !== null)
.sort((a, b) => {
const order = [
'official',
'opensource',
'article',
'video',
'feed',
];
return order.indexOf(a!.type) - order.indexOf(b!.type);
})
: [];
const title = rootHtml.querySelector('h1');
ulWithLinks?.remove();
title?.remove();
const allParagraphs = rootHtml.querySelectorAll('p');
if (listLinks.length > 0 && allParagraphs.length > 0) {
// to remove the view more see more from the description
const lastParagraph = allParagraphs[allParagraphs.length - 1];
lastParagraph?.remove();
}
const htmlStringWithoutLinks = rootHtml.toString();
const description = htmlToMarkdown(htmlStringWithoutLinks);
const updatedDescription =
`# ${title?.textContent}\n\n${description}`.trim();
const label = node?.data?.label as string;
if (!label) {
console.error(`🚨 Label is required: ${file}`);
continue;
}
topics.push({
roadmapSlug: roadmapId,
nodeId,
description: updatedDescription,
resources: listLinks,
});
}
await syncContentToDatabase(topics);
console.log(
`✅ Synced ${topics.length} topics to database for ${roadmapId}`,
);
} catch (error) {
console.error(error);
process.exit(1);
}
}

View File

@@ -0,0 +1,113 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { slugify } from '../src/lib/slugger';
import type { OfficialRoadmapDocument } from '../src/queries/official-roadmap';
import type { OfficialRoadmapTopicContentDocument } from '../src/queries/official-roadmap-topic';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const args = process.argv.slice(2);
const roadmapSlug = args?.[0]?.replace('--roadmap-slug=', '');
const secret = args?.[1]?.replace('--secret=', '');
if (!secret) {
throw new Error('Secret is required');
}
if (!roadmapSlug || roadmapSlug === '__default__') {
throw new Error('Roadmap slug is required');
}
console.log(`🚀 Starting ${roadmapSlug}`);
export async function roadmapTopics(
roadmapId: string,
secret: string,
): Promise<OfficialRoadmapTopicContentDocument[]> {
const path = `https://roadmap.sh/api/v1-list-official-roadmap-topics/${roadmapId}?secret=${secret}`;
const response = await fetch(path);
if (!response.ok) {
throw new Error(`Failed to fetch roadmap topics: ${response.statusText}`);
}
const data = await response.json();
if (data.error) {
throw new Error(`Failed to fetch roadmap topics: ${data.error}`);
}
return data;
}
export async function fetchRoadmapJson(
roadmapId: string,
): Promise<OfficialRoadmapDocument> {
const response = await fetch(
`https://roadmap.sh/api/v1-official-roadmap/${roadmapId}`,
);
if (!response.ok) {
throw new Error(`Failed to fetch roadmap json: ${response.statusText}`);
}
const data = await response.json();
if (data.error) {
throw new Error(`Failed to fetch roadmap json: ${data.error}`);
}
return data;
}
// Directory containing the roadmaps
const ROADMAP_CONTENT_DIR = path.join(
__dirname,
'../src/data/roadmaps',
roadmapSlug,
);
const allTopics = await roadmapTopics(roadmapSlug, secret);
const roadmap = await fetchRoadmapJson(roadmapSlug);
const { nodes } = roadmap;
for (const topic of allTopics) {
const { nodeId } = topic;
const node = nodes.find((node) => node.id === nodeId);
if (!node) {
console.error(`Node not found: ${nodeId}`);
continue;
}
const label = node?.data?.label as string;
if (!label) {
console.error(`Label not found: ${nodeId}`);
continue;
}
const topicSlug = `${slugify(label)}@${nodeId}.md`;
const topicPath = path.join(ROADMAP_CONTENT_DIR, 'content', topicSlug);
const topicDir = path.dirname(topicPath);
const topicDirExists = await fs
.stat(topicDir)
.then(() => true)
.catch(() => false);
if (!topicDirExists) {
await fs.mkdir(topicDir, { recursive: true });
}
const topicContent = prepareTopicContent(topic);
await fs.writeFile(topicPath, topicContent);
console.log(`✅ Synced ${topicSlug}`);
}
function prepareTopicContent(topic: OfficialRoadmapTopicContentDocument) {
const { description, resources = [] } = topic;
let content = description;
if (resources.length > 0) {
content += `\n\nVisit the following resources to learn more:\n\n${resources.map((resource) => `- [@${resource.type}@${resource.title}](${resource.url})`).join('\n')}`;
}
return content;
}

View File

@@ -0,0 +1,238 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import type { OfficialRoadmapDocument } from '../src/queries/official-roadmap';
import { parse } from 'node-html-parser';
import { markdownToHtml } from '../src/lib/markdown';
import { htmlToMarkdown } from '../src/lib/html';
import {
allowedOfficialRoadmapTopicResourceType,
type AllowedOfficialRoadmapTopicResourceType,
type SyncToDatabaseTopicContent,
} from '../src/queries/official-roadmap-topic';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const args = process.argv.slice(2);
const allFiles = args
.find((arg) => arg.startsWith('--files='))
?.replace('--files=', '');
const secret = args
.find((arg) => arg.startsWith('--secret='))
?.replace('--secret=', '');
if (!secret) {
throw new Error('Secret is required');
}
let roadmapJsonCache: Map<string, OfficialRoadmapDocument> = new Map();
export async function fetchRoadmapJson(
roadmapId: string,
): Promise<OfficialRoadmapDocument> {
if (roadmapJsonCache.has(roadmapId)) {
return roadmapJsonCache.get(roadmapId)!;
}
const response = await fetch(
`https://roadmap.sh/api/v1-official-roadmap/${roadmapId}`,
);
if (!response.ok) {
throw new Error(
`Failed to fetch roadmap json: ${response.statusText} for ${roadmapId}`,
);
}
const data = await response.json();
if (data.error) {
throw new Error(
`Failed to fetch roadmap json: ${data.error} for ${roadmapId}`,
);
}
roadmapJsonCache.set(roadmapId, data);
return data;
}
export async function syncContentToDatabase(
topics: SyncToDatabaseTopicContent[],
) {
const response = await fetch(
`https://roadmap.sh/api/v1-sync-official-roadmap-topics`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
topics,
secret,
}),
},
);
if (!response.ok) {
const error = await response.json();
throw new Error(
`Failed to sync content to database: ${response.statusText} ${JSON.stringify(error, null, 2)}`,
);
}
return response.json();
}
const files =
allFiles
?.split(',')
.map((file) => file.trim())
.filter(Boolean) || [];
if (files.length === 0) {
console.log('No files to sync');
process.exit(0);
}
console.log(`🚀 Starting ${files.length} files`);
const ROADMAP_CONTENT_DIR = path.join(__dirname, '../src/data/roadmaps');
try {
const topics: SyncToDatabaseTopicContent[] = [];
for (const file of files) {
const isContentFile = file.endsWith('.md') && file.includes('content/');
if (!isContentFile) {
console.log(`🚨 Skipping ${file} because it is not a content file`);
continue;
}
const pathParts = file.replace('src/data/roadmaps/', '').split('/');
const roadmapSlug = pathParts?.[0];
if (!roadmapSlug) {
console.error(`🚨 Roadmap slug is required: ${file}`);
continue;
}
const nodeSlug = pathParts?.[2]?.replace('.md', '');
if (!nodeSlug) {
console.error(`🚨 Node id is required: ${file}`);
continue;
}
const nodeId = nodeSlug.split('@')?.[1];
if (!nodeId) {
console.error(`🚨 Node id is required: ${file}`);
continue;
}
const roadmap = await fetchRoadmapJson(roadmapSlug);
const node = roadmap.nodes.find((node) => node.id === nodeId);
if (!node) {
console.error(`🚨 Node not found: ${file}`);
continue;
}
const filePath = path.join(
ROADMAP_CONTENT_DIR,
roadmapSlug,
'content',
`${nodeSlug}.md`,
);
const fileExists = await fs
.stat(filePath)
.then(() => true)
.catch(() => false);
if (!fileExists) {
console.log(`🚨 File not found: ${filePath}`);
continue;
}
const content = await fs.readFile(filePath, 'utf8');
const html = markdownToHtml(content, false);
const rootHtml = parse(html);
let ulWithLinks: HTMLElement | undefined;
rootHtml.querySelectorAll('ul').forEach((ul) => {
const listWithJustLinks = Array.from(ul.querySelectorAll('li')).filter(
(li) => {
const link = li.querySelector('a');
return link && link.textContent?.trim() === li.textContent?.trim();
},
);
if (listWithJustLinks.length > 0) {
// @ts-expect-error - TODO: fix this
ulWithLinks = ul;
}
});
const listLinks: SyncToDatabaseTopicContent['resources'] =
ulWithLinks !== undefined
? Array.from(ulWithLinks.querySelectorAll('li > a'))
.map((link) => {
const typePattern = /@([a-z.]+)@/;
let linkText = link.textContent || '';
const linkHref = link.getAttribute('href') || '';
let linkType = linkText.match(typePattern)?.[1] || 'article';
linkType = allowedOfficialRoadmapTopicResourceType.includes(
linkType as any,
)
? linkType
: 'article';
linkText = linkText.replace(typePattern, '');
return {
title: linkText,
url: linkHref,
type: linkType as AllowedOfficialRoadmapTopicResourceType,
};
})
.sort((a, b) => {
const order = [
'official',
'opensource',
'article',
'video',
'feed',
];
return order.indexOf(a.type) - order.indexOf(b.type);
})
: [];
const title = rootHtml.querySelector('h1');
ulWithLinks?.remove();
title?.remove();
const allParagraphs = rootHtml.querySelectorAll('p');
if (listLinks.length > 0 && allParagraphs.length > 0) {
// to remove the view more see more from the description
const lastParagraph = allParagraphs[allParagraphs.length - 1];
lastParagraph?.remove();
}
const htmlStringWithoutLinks = rootHtml.toString();
const description = htmlToMarkdown(htmlStringWithoutLinks);
const updatedDescription =
`# ${title?.textContent}\n\n${description}`.trim();
const label = node?.data?.label as string;
if (!label) {
console.error(`🚨 Label is required: ${file}`);
continue;
}
topics.push({
roadmapSlug,
nodeId,
description: updatedDescription,
resources: listLinks,
});
}
await syncContentToDatabase(topics);
} catch (error) {
console.error(error);
process.exit(1);
}

View File

@@ -1,45 +1,46 @@
---
import { DateTime } from 'luxon';
import type { ChangelogFileType } from '../../lib/changelog';
import ChangelogImages from '../ChangelogImages';
import type { ChangelogDocument } from '../../queries/changelog';
interface Props {
changelog: ChangelogFileType;
changelog: ChangelogDocument;
}
const { changelog } = Astro.props;
const { frontmatter } = changelog;
const formattedDate = DateTime.fromISO(frontmatter.date).toFormat(
const formattedDate = DateTime.fromISO(changelog.createdAt).toFormat(
'dd LLL, yyyy',
);
---
<div class='relative mb-6' id={changelog.id}>
<span
class='absolute -left-6 top-2 h-2 w-2 shrink-0 rounded-full bg-gray-300'
<div class='relative mb-6' id={changelog._id}>
<span class='absolute top-2 -left-6 h-2 w-2 shrink-0 rounded-full bg-gray-300'
></span>
<div class='mb-3 flex flex-col sm:flex-row items-start sm:items-center gap-0.5 sm:gap-2'>
<div
class='mb-3 flex flex-col items-start gap-0.5 sm:flex-row sm:items-center sm:gap-2'
>
<span class='shrink-0 text-xs tracking-wide text-gray-400'>
{formattedDate}
</span>
<span class='truncate text-base font-medium text-balance'>
{changelog.frontmatter.title}
{changelog.title}
</span>
</div>
<div class='rounded-xl border bg-white p-6'>
{frontmatter.images && (
<div class='mb-5 hidden sm:block -mx-6'>
<ChangelogImages images={frontmatter.images} client:load />
</div>
)}
{
changelog.images && (
<div class='-mx-6 mb-5 hidden sm:block'>
<ChangelogImages images={changelog.images} client:load />
</div>
)
}
<div
class='prose prose-sm prose-h2:mt-3 prose-h2:text-lg prose-h2:font-medium prose-p:mb-0 prose-blockquote:font-normal prose-blockquote:text-gray-500 prose-ul:my-0 prose-ul:rounded-lg prose-ul:bg-gray-100 prose-ul:px-4 prose-ul:py-4 prose-ul:pl-7 prose-img:mt-0 prose-img:rounded-lg [&>blockquote>p]:mt-0 [&>ul>li]:my-0 [&>ul>li]:mb-1 [&>ul]:mt-3'
>
<changelog.Content />
</div>
class='prose prose-sm [&_li_p]:my-0 prose-h2:mt-3 prose-h2:text-lg prose-h2:font-medium prose-p:mb-0 prose-blockquote:font-normal prose-blockquote:text-gray-500 prose-ul:my-0 prose-ul:rounded-lg prose-ul:bg-gray-100 prose-ul:px-4 prose-ul:py-4 prose-ul:pl-7 prose-img:mt-0 prose-img:rounded-lg [&>blockquote>p]:mt-0 [&>ul]:mt-3 [&>ul>li]:my-0 [&>ul>li]:mb-1'
set:html={changelog.description}
/>
</div>
</div>

View File

@@ -1,9 +1,9 @@
---
import { getAllChangelogs } from '../lib/changelog';
import { listChangelog } from '../queries/changelog';
import { DateTime } from 'luxon';
import AstroIcon from './AstroIcon.astro';
const allChangelogs = await getAllChangelogs();
const top10Changelogs = allChangelogs.slice(0, 10);
const changelogs = await listChangelog({ limit: 10 });
---
<div class='border-t bg-white py-6 text-left sm:py-16 sm:text-center'>
@@ -17,7 +17,7 @@ const top10Changelogs = allChangelogs.slice(0, 10);
Actively Maintained
</p>
<p
class='mb-2 mt-1 text-sm leading-relaxed text-gray-600 sm:my-2 sm:my-5 sm:text-lg'
class='mt-1 mb-2 text-sm leading-relaxed text-gray-600 sm:my-5 sm:text-lg'
>
We are always improving our content, adding new resources and adding
features to enhance your learning experience.
@@ -25,27 +25,27 @@ const top10Changelogs = allChangelogs.slice(0, 10);
<div class='relative mt-2 text-left sm:mt-8'>
<div
class='absolute inset-y-0 left-[120px] hidden w-px -translate-x-[0.5px] translate-x-[5.75px] bg-gray-300 sm:block'
class='absolute inset-y-0 left-[120px] hidden w-px translate-x-[5.75px] bg-gray-300 sm:block'
>
</div>
<ul class='relative flex flex-col gap-4 py-4'>
{
top10Changelogs.map((changelog) => {
changelogs.map((changelog) => {
const formattedDate = DateTime.fromISO(
changelog.frontmatter.date,
changelog.createdAt,
).toFormat('dd LLL, yyyy');
return (
<li class='relative'>
<a
href={`/changelog#${changelog.id}`}
href={`/changelog#${changelog._id}`}
class='flex flex-col items-start sm:flex-row sm:items-center'
>
<span class='shrink-0 pr-0 text-right text-sm tracking-wide text-gray-400 sm:w-[120px] sm:pr-4'>
{formattedDate}
</span>
<span class='hidden h-3 w-3 shrink-0 rounded-full bg-gray-300 sm:block' />
<span class='text-balance text-base font-medium text-gray-900 sm:pl-8'>
{changelog.frontmatter.title}
<span class='text-base font-medium text-balance text-gray-900 sm:pl-8'>
{changelog.title}
</span>
</a>
</li>
@@ -55,7 +55,7 @@ const top10Changelogs = allChangelogs.slice(0, 10);
</ul>
</div>
<div
class='mt-2 flex flex-col gap-2 sm:flex-row sm:mt-8 sm:items-center sm:justify-center'
class='mt-2 flex flex-col gap-2 sm:mt-8 sm:flex-row sm:items-center sm:justify-center'
>
<a
href='/changelog'
@@ -66,7 +66,7 @@ const top10Changelogs = allChangelogs.slice(0, 10);
<button
data-guest-required
data-popup='login-popup'
class='flex flex-row items-center gap-2 rounded-lg border border-black bg-white px-4 py-2 text-sm text-black transition-all hover:bg-black hover:text-white sm:rounded-full sm:pl-4 sm:pr-5 sm:text-base'
class='flex flex-row items-center gap-2 rounded-lg border border-black bg-white px-4 py-2 text-sm text-black transition-all hover:bg-black hover:text-white sm:rounded-full sm:pr-5 sm:pl-4 sm:text-base'
>
<AstroIcon icon='bell' class='h-5 w-5' />
Subscribe for Notifications

View File

@@ -1,13 +1,14 @@
import { ChevronLeft, ChevronRight } from 'lucide-react';
import React, { useState, useEffect, useCallback } from 'react';
import type { ChangelogImage } from '../queries/changelog';
interface ChangelogImagesProps {
images: { [key: string]: string };
images: ChangelogImage[];
}
const ChangelogImages: React.FC<ChangelogImagesProps> = ({ images }) => {
const [enlargedImage, setEnlargedImage] = useState<string | null>(null);
const imageArray = Object.entries(images);
const imageArray = images.map((image) => [image.title, image.url]);
const handleImageClick = (src: string) => {
setEnlargedImage(src);
@@ -63,10 +64,10 @@ const ChangelogImages: React.FC<ChangelogImagesProps> = ({ images }) => {
alt={title}
className="h-[120px] w-full object-cover object-left-top"
/>
<span className="absolute group-hover:opacity-0 inset-0 bg-linear-to-b from-transparent to-black/40" />
<span className="absolute inset-0 bg-linear-to-b from-transparent to-black/40 group-hover:opacity-0" />
<div className="absolute font-medium inset-x-0 top-full group-hover:inset-y-0 flex items-center justify-center px-2 text-center text-xs bg-black/50 text-white py-0.5 opacity-0 group-hover:opacity-100 cursor-pointer">
<span className='bg-black py-0.5 rounded-sm px-1'>{title}</span>
<div className="absolute inset-x-0 top-full flex cursor-pointer items-center justify-center bg-black/50 px-2 py-0.5 text-center text-xs font-medium text-white opacity-0 group-hover:inset-y-0 group-hover:opacity-100">
<span className="rounded-sm bg-black px-1 py-0.5">{title}</span>
</div>
</div>
))}
@@ -82,7 +83,7 @@ const ChangelogImages: React.FC<ChangelogImagesProps> = ({ images }) => {
className="max-h-[90%] max-w-[90%] rounded-xl object-contain"
/>
<button
className="absolute left-4 top-1/2 -translate-y-1/2 transform rounded-full bg-white/50 hover:bg-white/100 p-2"
className="absolute top-1/2 left-4 -translate-y-1/2 transform rounded-full bg-white/50 p-2 hover:bg-white/100"
onClick={(e) => {
e.stopPropagation();
handleNavigation('prev');
@@ -91,7 +92,7 @@ const ChangelogImages: React.FC<ChangelogImagesProps> = ({ images }) => {
<ChevronLeft size={24} />
</button>
<button
className="absolute right-4 top-1/2 -translate-y-1/2 transform rounded-full bg-white/50 hover:bg-white/100 p-2"
className="absolute top-1/2 right-4 -translate-y-1/2 transform rounded-full bg-white/50 p-2 hover:bg-white/100"
onClick={(e) => {
e.stopPropagation();
handleNavigation('next');

View File

@@ -0,0 +1,131 @@
.ai-chat {
.prose ul li > code,
.prose ol li > code,
p code,
a > code,
strong > code,
em > code,
h1 > code,
h2 > code,
h3 > code {
background: #ebebeb !important;
color: currentColor !important;
font-size: 14px;
font-weight: normal !important;
}
.message-markdown.prose ul li > code,
.message-markdown.prose ol li > code,
.message-markdown.prose p code,
.message-markdown.prose a > code,
.message-markdown.prose strong > code,
.message-markdown.prose em > code,
.message-markdown.prose h1 > code,
.message-markdown.prose h2 > code,
.message-markdown.prose h3 > code {
font-size: 12px !important;
}
.message-markdown pre {
-ms-overflow-style: none;
scrollbar-width: none;
}
.message-markdown pre::-webkit-scrollbar {
display: none;
}
.message-markdown pre,
.message-markdown pre {
overflow: scroll;
}
.prose ul li > code:before,
p > code:before,
.prose ul li > code:after,
.prose ol li > code:before,
p > code:before,
.prose ol li > code:after,
.message-markdown h1 > code:after,
.message-markdown h1 > code:before,
.message-markdown h2 > code:after,
.message-markdown h2 > code:before,
.message-markdown h3 > code:after,
.message-markdown h3 > code:before,
.message-markdown h4 > code:after,
.message-markdown h4 > code:before,
p > code:after,
a > code:after,
a > code:before {
content: '' !important;
}
.message-markdown.prose ul li > code,
.message-markdown.prose ol li > code,
.message-markdown p code,
.message-markdown a > code,
.message-markdown strong > code,
.message-markdown em > code,
.message-markdown h1 > code,
.message-markdown h2 > code,
.message-markdown h3 > code,
.message-markdown table code {
background: #f4f4f5 !important;
border: 1px solid #282a36 !important;
color: #282a36 !important;
padding: 2px 4px;
border-radius: 5px;
white-space: pre;
font-weight: normal;
}
.message-markdown blockquote {
font-style: normal;
}
.message-markdown.prose blockquote h1,
.message-markdown.prose blockquote h2,
.message-markdown.prose blockquote h3,
.message-markdown.prose blockquote h4 {
font-style: normal;
margin-bottom: 8px;
}
.message-markdown.prose ul li > code:before,
.message-markdown p > code:before,
.message-markdown.prose ul li > code:after,
.message-markdown p > code:after,
.message-markdown h2 > code:after,
.message-markdown h2 > code:before,
.message-markdown table code:before,
.message-markdown table code:after,
.message-markdown a > code:after,
.message-markdown a > code:before,
.message-markdown h2 code:after,
.message-markdown h2 code:before,
.message-markdown h2 code:after,
.message-markdown h2 code:before {
content: '' !important;
}
.message-markdown table {
border-collapse: collapse;
border: 1px solid black;
border-radius: 5px;
}
.message-markdown table td,
.message-markdown table th {
padding: 5px 10px;
}
.chat-variable {
font-size: 12px;
font-weight: 500;
line-height: 1.5;
padding: 2px 4px;
border-radius: 8px;
background-color: #f0f5ff;
color: #2c5df1;
}
}

View File

@@ -0,0 +1,108 @@
import { useQuery } from '@tanstack/react-query';
import { officialRoadmapOptions } from '../../queries/official-roadmap';
import { queryClient } from '../../stores/query-client';
type RoadmapChatIntroMessageProps = {
roadmapId: string;
};
export function RoadmapChatIntroMessage(props: RoadmapChatIntroMessageProps) {
const { roadmapId } = props;
const { data: roadmapDetail } = useQuery(
officialRoadmapOptions(roadmapId),
queryClient,
);
const topicNodes = roadmapDetail?.nodes?.filter(
(node) => node.type === 'topic',
);
const firstTopicNode = topicNodes?.[0];
const firstTopicTitle = firstTopicNode?.data?.label || 'XYZ';
const secondTopicNode = topicNodes?.[1];
const secondTopicTitle = secondTopicNode?.data?.label || 'XYZ';
const capabilities = [
{
icon: '📚',
title: 'Learn concepts:',
description: 'Ask me about any topics on the roadmap',
examples:
'"Explain what React hooks are" or "How does async/await work?"',
},
{
icon: '📊',
title: 'Track progress:',
description: 'Mark topics as done, learning, or skipped',
examples: `"Mark ${firstTopicTitle} as done" or "Show my overall progress"`,
},
{
icon: '🎯',
title: 'Recommendations:',
description: 'Find what to learn next or explore other roadmaps',
examples: `"What should I learn next?" or "Recommend roadmaps for backend development"`,
},
{
icon: '🔍',
title: 'Find resources:',
description: 'Get learning materials for specific topics',
examples: `"Show me resources for learning ${secondTopicTitle}"`,
},
{
icon: '🔗',
title: 'Share progress:',
description: 'Get a link to share your learning progress',
examples: '"Give me my shareable progress link"',
},
];
return (
<div className="space-y-2 text-sm text-gray-700">
<div className="flex items-start gap-3">
<div>
<h3 className="mb-2 font-medium text-gray-900">
Hi! I'm your AI learning assistant 👋
</h3>
<p className="mb-3">
I'm here to guide you through your learning journey on this roadmap.
I can help you understand concepts, track your progress, and provide
personalized learning advice.
</p>
</div>
</div>
<div className="space-y-3">
<h4 className="font-medium text-gray-900">
Here's what I can help you with:
</h4>
<div className="space-y-3">
{capabilities.map((capability, index) => (
<div key={index} className="flex items-start gap-2">
<span className={`font-medium`}>{capability.icon}</span>
<div>
<span className="font-medium text-black">
{capability.title}
</span>{' '}
{capability.description}
<div className="mt-1 text-xs text-gray-600">
Try: {capability.examples}
</div>
</div>
</div>
))}
</div>
</div>
<div className="mt-4 rounded-lg bg-gray-50 p-3">
<p className="text-xs text-black">
<span className="font-medium">Tip:</span> I can see your current
progress on the roadmap, so my advice will be personalized to your
learning journey. Just ask me anything about the topics you see on the
roadmap!
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,148 @@
import { Markdown } from '../Global/Markdown';
import { BotIcon, User2Icon } from 'lucide-react';
import type { UIMessage } from 'ai';
import { parseMessageParts } from '../../lib/message-part';
import { RoadmapChatUserProgressList } from './UserProgressList';
import {
parseUserProgress,
UserProgressActionList,
} from './UserPrgressActionList';
import { parseTopicList, RoadmapTopicList } from './RoadmapTopicList';
import { ShareResourceLink } from './ShareResourceLink';
import {
parseRoadmapSlugList,
RoadmapRecommendations,
} from './RoadmapRecommendations';
import { cn } from '../../lib/classname';
type RoadmapMessageProps = {
roadmapId: string;
message: UIMessage;
isStreaming: boolean;
children?: React.ReactNode;
onTopicClick?: (topicId: string, topicTitle: string) => void;
};
export function RoadmapChatMessage(props: RoadmapMessageProps) {
const { roadmapId, message, isStreaming, children, onTopicClick } = props;
const { role } = message;
return (
<div
className={cn(
'flex flex-col rounded-lg',
role === 'user' ? 'bg-gray-300/30' : 'bg-yellow-500/30',
)}
>
<div className="flex items-start gap-2.5 p-3">
<div
className={cn(
'flex size-6 shrink-0 items-center justify-center rounded-full',
role === 'user'
? 'bg-gray-200 text-black'
: 'bg-yellow-400 text-black',
)}
>
{role === 'user' ? (
<User2Icon className="size-4 stroke-[2.5]" />
) : (
<BotIcon className="size-4 stroke-[2.5]" />
)}
</div>
{children || (
<div>
{message.parts.map((part) => {
const { type } = part;
if (role === 'user' && type === 'text') {
return (
<div
key={`message-${message.id}-part-${type}`}
className="prose prose-sm message-markdown max-w-full text-sm"
dangerouslySetInnerHTML={{ __html: part.text ?? '' }}
/>
);
}
if (type === 'text') {
const text = part.text;
const parts = parseMessageParts(text, {
'user-progress': () => {
return {};
},
'update-progress': (opts) => {
return parseUserProgress(opts.content);
},
'roadmap-topics': (opts) => {
return parseTopicList(opts.content);
},
'resource-progress-link': () => {
return {};
},
'roadmap-recommendations': (opts) => {
return parseRoadmapSlugList(opts.content);
},
});
return parts.map((part, index) => {
const { type } = part;
const key = `message-${message.id}-part-${type}-${index}`;
if (type === 'text') {
return (
<Markdown
key={key}
className="prose prose-sm message-markdown max-w-full text-sm"
>
{part.text ?? ''}
</Markdown>
);
} else if (type === 'user-progress') {
return (
<RoadmapChatUserProgressList
key={key}
roadmapId={roadmapId}
/>
);
} else if (type === 'update-progress') {
return (
<UserProgressActionList
key={key}
roadmapId={roadmapId}
updateUserProgress={part.data}
isLoading={isStreaming}
/>
);
} else if (type === 'roadmap-topics') {
return (
<RoadmapTopicList
key={key}
roadmapId={roadmapId}
topics={part.data}
onTopicClick={onTopicClick}
/>
);
} else if (type === 'resource-progress-link') {
return (
<ShareResourceLink key={key} roadmapId={roadmapId} />
);
} else if (type === 'roadmap-recommendations') {
return (
<RoadmapRecommendations
key={key}
roadmapSlugs={part.data}
/>
);
}
return null;
});
}
})}
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,110 @@
import type { ChatStatus, UIMessage } from 'ai';
import { memo } from 'react';
import { RoadmapChatMessage } from './RoadmapChatMessage';
import { useIsThinking } from '../../hooks/use-is-thinking';
type MessagesProps = {
messages: UIMessage[];
status: ChatStatus;
roadmapId: string;
onTopicClick?: (topicId: string, topicTitle: string) => void;
defaultQuestions?: string[];
onDefaultQuestionClick?: (question: string) => void;
};
function _RoadmapChatMessages(props: MessagesProps) {
const {
messages,
status,
roadmapId,
defaultQuestions,
onTopicClick,
onDefaultQuestionClick,
} = props;
const isStreaming = status === 'streaming';
const isThinking = useIsThinking(messages, status);
return (
<div className="absolute inset-0 flex flex-col">
<div className="relative flex grow flex-col justify-end">
<div className="flex flex-col justify-end gap-2 px-3 py-2">
<RoadmapChatMessage
roadmapId={roadmapId}
message={{
id: '__welcome_message__',
role: 'assistant',
parts: [
{
type: 'text',
text: 'Hello, how can I help you today?',
},
],
}}
isStreaming={isStreaming}
/>
{messages.length === 0 &&
defaultQuestions &&
defaultQuestions.length > 0 && (
<div className="mt-0.5 mb-1">
<p className="mb-2 text-xs font-normal text-gray-500">
Some questions you might have about this roadmap:
</p>
<div className="flex flex-col justify-end gap-1">
{defaultQuestions.map((question, index) => (
<button
key={`default-question-${index}`}
className="flex h-full self-start rounded-md bg-yellow-500/10 px-3 py-2 text-left text-sm text-black hover:bg-yellow-500/20"
onClick={() => onDefaultQuestionClick?.(question)}
>
{question}
</button>
))}
</div>
</div>
)}
{messages.map((message, index) => {
const isLastMessage = index === messages.length - 1;
// otherwise it will add an extra space at the end of the message
// because the last message is not rendered
if (isThinking && isLastMessage && message.role === 'assistant') {
return null;
}
return (
<RoadmapChatMessage
key={message.id}
roadmapId={roadmapId}
message={message}
isStreaming={isStreaming}
onTopicClick={onTopicClick}
/>
);
})}
{isThinking && (
<RoadmapChatMessage
roadmapId={roadmapId}
message={{
id: '__thinking_message__',
role: 'assistant',
parts: [
{
type: 'text',
text: 'Thinking...',
},
],
}}
isStreaming={isStreaming}
/>
)}
</div>
</div>
</div>
);
}
export const RoadmapChatMessages = memo(_RoadmapChatMessages);

View File

@@ -0,0 +1,82 @@
import { useQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import { Loader2Icon, SquareArrowOutUpRightIcon } from 'lucide-react';
import { listBuiltInRoadmaps } from '../../queries/roadmap';
import { queryClient } from '../../stores/query-client';
type RoadmapSlugListType = {
roadmapSlug: string;
};
export function parseRoadmapSlugList(content: string): RoadmapSlugListType[] {
const items: RoadmapSlugListType[] = [];
const roadmapSlugListRegex = /<roadmap-slug>.*?<\/roadmap-slug>/gs;
const roadmapSlugListItems = content.match(roadmapSlugListRegex);
if (!roadmapSlugListItems) {
return items;
}
for (const roadmapSlugListItem of roadmapSlugListItems) {
const roadmapSlugRegex = /<roadmap-slug>(.*?)<\/roadmap-slug>/;
const roadmapSlug = roadmapSlugListItem
.match(roadmapSlugRegex)?.[1]
?.trim();
if (!roadmapSlug) {
continue;
}
items.push({
roadmapSlug,
});
}
return items;
}
type RoadmapRecommendationsProps = {
roadmapSlugs: RoadmapSlugListType[];
};
export function RoadmapRecommendations(props: RoadmapRecommendationsProps) {
const { roadmapSlugs } = props;
const { data: roadmaps, isLoading } = useQuery(
listBuiltInRoadmaps(),
queryClient,
);
const progressItemWithText = useMemo(() => {
return roadmapSlugs.map((item) => {
const roadmap = roadmaps?.find(
(mapping) => mapping.id === item.roadmapSlug,
);
return {
...item,
title: roadmap?.title,
};
});
}, [roadmapSlugs, roadmaps]);
return (
<div className="relative my-6 flex flex-wrap gap-1 first:mt-0 last:mb-0">
{progressItemWithText.map((item) => (
<a
href={`/${item.roadmapSlug}/ai`}
target="_blank"
key={item.roadmapSlug}
className="group flex h-[34px] items-center gap-2 rounded-lg border border-gray-200 bg-white px-2.5 py-1.5 text-left text-sm text-gray-700 transition-all hover:border-gray-400 hover:text-black active:bg-gray-100"
>
{item.title}
{isLoading && (
<Loader2Icon className="size-3.5 animate-spin text-gray-400 group-hover:text-gray-600" />
)}
{!isLoading && (
<SquareArrowOutUpRightIcon className="ml-1 size-3.5 text-gray-400 transition-transform group-hover:text-gray-600" />
)}
</a>
))}
</div>
);
}

View File

@@ -0,0 +1,106 @@
import { useQuery } from '@tanstack/react-query';
import { Fragment, useMemo } from 'react';
import { ChevronRightIcon } from 'lucide-react';
import { roadmapTreeMappingOptions } from '../../queries/roadmap-tree';
import { queryClient } from '../../stores/query-client';
type TopicListType = {
topicId: string;
};
export function parseTopicList(content: string): TopicListType[] {
const items: TopicListType[] = [];
const topicListRegex = /<topic-id>.*?<\/topic-id>/gs;
const topicListItems = content.match(topicListRegex);
if (!topicListItems) {
return items;
}
for (const topicListItem of topicListItems) {
const topicIdRegex = /<topic-id>(.*?)<\/topic-id>/;
const topicId = topicListItem.match(topicIdRegex)?.[1]?.trim();
if (!topicId) {
continue;
}
items.push({
topicId,
});
}
return items;
}
type RoadmapTopicListProps = {
roadmapId: string;
onTopicClick?: (topicId: string, topicTitle: string) => void;
topics: TopicListType[];
};
export function RoadmapTopicList(props: RoadmapTopicListProps) {
const { roadmapId, topics: topicListItems, onTopicClick } = props;
const { data: roadmapTreeData } = useQuery(
roadmapTreeMappingOptions(roadmapId),
queryClient,
);
const progressItemWithText = useMemo(() => {
return topicListItems.map((item) => {
const roadmapTreeItem = roadmapTreeData?.find(
(mapping) => mapping.nodeId === item.topicId,
);
return {
...item,
text: (roadmapTreeItem?.text || item.topicId)
?.split(' > ')
.slice(1)
.join(' > '),
};
});
}, [topicListItems, roadmapTreeData]);
return (
<div className="relative my-6 flex flex-wrap gap-1 first:mt-0 last:mb-0">
{progressItemWithText.map((item) => {
const labelParts = item.text.split(' > ').slice(-2);
const labelPartCount = labelParts.length;
const title = item.text.split(' > ').pop();
if (!title) {
return;
}
return (
<button
key={item.topicId}
className="collapse-if-empty flex items-center gap-1 rounded-lg border border-gray-200 bg-white p-1 px-2 text-left text-sm hover:bg-gray-50"
onClick={() => {
if (!title) {
return;
}
onTopicClick?.(item.topicId, title);
}}
>
{labelParts.map((part, index) => {
return (
<Fragment key={index}>
<span>{part}</span>
{index < labelPartCount - 1 && (
<ChevronRightIcon
className="size-3 text-gray-400"
strokeWidth={2.5}
/>
)}
</Fragment>
);
})}
</button>
);
})}
</div>
);
}

View File

@@ -0,0 +1,47 @@
import { ShareIcon } from 'lucide-react';
import { useCopyText } from '../../hooks/use-copy-text';
import { cn } from '../../lib/classname';
import { useAuth } from '../../hooks/use-auth';
import { CheckIcon } from '../ReactIcons/CheckIcon';
type ShareResourceLinkProps = {
roadmapId: string;
};
export function ShareResourceLink(props: ShareResourceLinkProps) {
const { roadmapId } = props;
const currentUser = useAuth();
const { copyText, isCopied } = useCopyText();
const handleShareResourceLink = () => {
const url = `${import.meta.env.VITE_ASTRO_APP_URL}/${roadmapId}?s=${currentUser?.id}`;
copyText(url);
};
return (
<div className="relative my-6 flex flex-wrap gap-1 first:mt-0 last:mb-0">
<button
className={cn(
'flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white p-1 px-1.5 text-left text-sm',
isCopied && 'text-green-500',
)}
onClick={handleShareResourceLink}
>
{!isCopied && (
<>
<ShareIcon className="h-4 w-4" />
Share Progress
</>
)}
{isCopied && (
<>
<CheckIcon additionalClasses="size-4" />
Copied
</>
)}
</button>
</div>
);
}

View File

@@ -0,0 +1,63 @@
import { cn } from '../../lib/classname';
import { Markdown } from '../Global/Markdown';
import { BotIcon, User2Icon } from 'lucide-react';
import type { UIMessage } from 'ai';
import { promptLabelMapping } from '../TopicDetail/PredefinedActions';
type TopicChatMessageProps = {
message: UIMessage;
};
export function TopicChatMessage(props: TopicChatMessageProps) {
const { message } = props;
const { role } = message;
return (
<div
className={cn(
'flex flex-col rounded-lg',
role === 'user' ? 'bg-gray-300/30' : 'bg-yellow-500/30',
)}
>
<div className="flex items-start gap-2.5 p-3">
<div
className={cn(
'flex size-6 shrink-0 items-center justify-center rounded-full',
role === 'user'
? 'bg-gray-200 text-black'
: 'bg-yellow-400 text-black',
)}
>
{role === 'user' ? (
<User2Icon className="size-4 stroke-[2.5]" />
) : (
<BotIcon className="size-4 stroke-[2.5]" />
)}
</div>
<div>
{message.parts.map((part) => {
const { type } = part;
const key = `message-${message.id}-part-${type}`;
if (type === 'text') {
let content = part.text;
if (role === 'user' && promptLabelMapping[content]) {
content = promptLabelMapping[content];
}
return (
<Markdown
key={key}
className="prose prose-sm message-markdown max-w-full text-sm"
>
{content}
</Markdown>
);
}
})}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,57 @@
import type { ChatStatus, UIMessage } from 'ai';
import { TopicChatMessage } from './TopicChatMessage';
import { useIsThinking } from '../../hooks/use-is-thinking';
type TopicChatMessagesProps = {
messages: UIMessage[];
status: ChatStatus;
};
export function TopicChatMessages(props: TopicChatMessagesProps) {
const { messages, status } = props;
const isThinking = useIsThinking(messages, status);
return (
<div className="absolute inset-0 flex flex-col">
<div className="relative flex grow flex-col justify-end">
<div className="flex flex-col justify-end gap-2 px-3 py-2">
<TopicChatMessage
message={{
id: '__welcome_message__',
role: 'assistant',
parts: [
{
type: 'text',
text: 'Hey, I am your AI instructor. How can I help you today? 🤖',
},
],
}}
/>
{messages.map((message, index) => {
const isLastMessage = index === messages.length - 1;
// otherwise it will add an extra space at the end of the message
// because the last message is not rendered
if (isThinking && isLastMessage && message.role === 'assistant') {
return null;
}
return <TopicChatMessage key={message.id} message={message} />;
})}
{isThinking && (
<TopicChatMessage
message={{
id: '__thinking_message__',
role: 'assistant',
parts: [{ type: 'text', text: 'Thinking...' }],
}}
/>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,330 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { ChevronRightIcon, Loader2Icon } from 'lucide-react';
import { CheckIcon } from '../ReactIcons/CheckIcon';
import { Fragment, useMemo, useState } from 'react';
import { roadmapTreeMappingOptions } from '../../queries/roadmap-tree';
import { queryClient } from '../../stores/query-client';
import { httpPost } from '../../lib/query-http';
import {
renderTopicProgress,
updateResourceProgress,
type ResourceProgressType,
} from '../../lib/resource-progress';
import { userResourceProgressOptions } from '../../queries/resource-progress';
import { useToast } from '../../hooks/use-toast';
import { cn } from '../../lib/classname';
type UpdateUserProgress = {
id: string;
action: 'done' | 'learning' | 'skipped' | 'pending';
};
export function parseUserProgress(content: string): UpdateUserProgress[] {
const items: UpdateUserProgress[] = [];
const progressRegex = /<update-progress-item>.*?<\/update-progress-item>/gs;
const progressItems = content.match(progressRegex);
if (!progressItems) {
return items;
}
for (const progressItem of progressItems) {
const progressItemRegex = /<topic-id>(.*?)<\/topic-id>/;
const topicId = progressItem.match(progressItemRegex)?.[1]?.trim();
const topicActionRegex = /<topic-action>(.*?)<\/topic-action>/;
const topicAction = progressItem
.match(topicActionRegex)?.[1]
.trim()
?.toLowerCase();
if (!topicId || !topicAction) {
continue;
}
items.push({
id: topicId,
action: topicAction as UpdateUserProgress['action'],
});
}
return items;
}
type BulkUpdateResourceProgressBody = {
done: string[];
learning: string[];
skipped: string[];
pending: string[];
};
type BulkUpdateResourceProgressResponse = {
done: string[];
learning: string[];
skipped: string[];
};
type UserProgressActionListProps = {
roadmapId: string;
isLoading?: boolean;
updateUserProgress: UpdateUserProgress[];
};
export function UserProgressActionList(props: UserProgressActionListProps) {
const { roadmapId, updateUserProgress, isLoading = false } = props;
const toast = useToast();
const { data: roadmapTreeData } = useQuery(
roadmapTreeMappingOptions(roadmapId),
queryClient,
);
const {
mutate: bulkUpdateResourceProgress,
isPending: isBulkUpdating,
isSuccess: isBulkUpdateSuccess,
} = useMutation(
{
mutationFn: (body: BulkUpdateResourceProgressBody) => {
return httpPost<BulkUpdateResourceProgressResponse>(
`/v1-bulk-update-resource-progress/${roadmapId}`,
body,
);
},
onSuccess: () => {
updateUserProgress.forEach((item) => {
renderTopicProgress(item.id, item.action);
});
return queryClient.invalidateQueries(
userResourceProgressOptions('roadmap', roadmapId),
);
},
onError: (error) => {
toast.error(
error?.message ?? 'Something went wrong, please try again.',
);
},
},
queryClient,
);
const progressItemWithText = useMemo(() => {
return updateUserProgress.map((item) => {
const roadmapTreeItem = roadmapTreeData?.find(
(mapping) => mapping.nodeId === item.id,
);
return {
...item,
text: (roadmapTreeItem?.text || item.id)
?.split(' > ')
.slice(1)
.join(' > '),
};
});
}, [updateUserProgress, roadmapTreeData]);
const [showAll, setShowAll] = useState(false);
const itemCountToShow = 4;
const itemsToShow = showAll
? progressItemWithText
: progressItemWithText.slice(0, itemCountToShow);
const hasMoreItemsToShow = progressItemWithText.length > itemCountToShow;
return (
<div className="relative my-6 w-full first:mt-0 last:mb-0">
<div className="relative flex flex-col gap-0.5">
{itemsToShow.map((item) => (
<ProgressItem
key={item.id}
roadmapId={roadmapId}
topicId={item.id}
text={item.text}
action={item.action}
isStreaming={isLoading}
isBulkUpdating={isBulkUpdating}
isBulkUpdateSuccess={isBulkUpdateSuccess}
/>
))}
{hasMoreItemsToShow && (
<div className="relative mt-1 flex items-center justify-between gap-2">
<button
className="z-50 flex items-center gap-1 rounded-md bg-gray-400 px-2 py-1 text-xs font-medium text-white hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-70"
onClick={() => setShowAll(!showAll)}
disabled={isLoading}
>
{isLoading && (
<>
<Loader2Icon className="size-3 animate-spin" />
{progressItemWithText.length} loaded ..
</>
)}
{!isLoading && (
<>
{showAll
? '- Show Less'
: `+ Show ${progressItemWithText.length - itemCountToShow} More`}
</>
)}
</button>
<button
className="z-50 flex items-center gap-1 rounded-md bg-green-600 px-2 py-1 text-xs font-medium text-white hover:bg-green-700 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-70"
disabled={isBulkUpdating || isLoading || isBulkUpdateSuccess}
onClick={() => {
const done = updateUserProgress
.filter((item) => item.action === 'done')
.map((item) => item.id);
const learning = updateUserProgress
.filter((item) => item.action === 'learning')
.map((item) => item.id);
const skipped = updateUserProgress
.filter((item) => item.action === 'skipped')
.map((item) => item.id);
const pending = updateUserProgress
.filter((item) => item.action === 'pending')
.map((item) => item.id);
bulkUpdateResourceProgress({
done,
learning,
skipped,
pending,
});
}}
>
{isBulkUpdating && (
<Loader2Icon className="size-3 animate-spin" />
)}
{!isBulkUpdating && <CheckIcon additionalClasses="size-3" />}
Apply All
</button>
</div>
)}
</div>
</div>
);
}
type ProgressItemProps = {
roadmapId: string;
topicId: string;
text: string;
action: UpdateUserProgress['action'];
isStreaming: boolean;
isBulkUpdating: boolean;
isBulkUpdateSuccess: boolean;
};
function ProgressItem(props: ProgressItemProps) {
const {
roadmapId,
topicId,
text,
action,
isStreaming,
isBulkUpdating,
isBulkUpdateSuccess,
} = props;
const toast = useToast();
const {
mutate: updateTopicStatus,
isSuccess,
isPending: isUpdating,
} = useMutation(
{
mutationFn: (action: ResourceProgressType) => {
return updateResourceProgress(
{
resourceId: roadmapId,
resourceType: 'roadmap',
topicId,
},
action,
);
},
onMutate: () => {},
onSuccess: () => {
renderTopicProgress(topicId, action);
},
onError: () => {
toast.error('Something went wrong, please try again.');
},
onSettled: () => {
return queryClient.invalidateQueries(
userResourceProgressOptions('roadmap', roadmapId),
);
},
},
queryClient,
);
const textParts = text.split(' > ');
const lastIndex = textParts.length - 1;
return (
<div className="flex min-h-[40px] items-center justify-between gap-2 rounded-lg border border-gray-200 bg-white py-1 pr-1 pl-3">
<span className="flex items-center gap-1 truncate text-sm text-gray-500">
{textParts.map((part, index) => {
return (
<Fragment key={index}>
{part}
{index !== lastIndex && (
<span className="text-gray-500">
<ChevronRightIcon className="size-3 shrink-0" />{' '}
</span>
)}
</Fragment>
);
})}
</span>
{!isSuccess && !isBulkUpdateSuccess && (
<>
{!isStreaming && (
<button
className={cn(
`flex shrink-0 items-center gap-1.5 rounded-md border border-gray-200 px-2 py-1 text-xs disabled:pointer-events-none disabled:opacity-40`,
{
'bg-green-100 hover:border-green-300 hover:bg-green-200':
action === 'done',
'bg-yellow-100 hover:border-yellow-300 hover:bg-yellow-200':
action === 'learning',
'bg-gray-800 text-white hover:border-black hover:bg-black':
action === 'skipped',
'bg-gray-100 hover:border-gray-300 hover:bg-gray-200':
action === 'pending',
},
)}
onClick={() => updateTopicStatus(action)}
disabled={isStreaming || isUpdating || isBulkUpdating}
>
{(isUpdating || isBulkUpdating) && (
<Loader2Icon className="size-4 animate-spin" />
)}
{!isUpdating && !isBulkUpdating && (
<>
<CheckIcon additionalClasses="size-3" />
Mark it as {action}
</>
)}
</button>
)}
{isStreaming && (
<span className="flex size-[30px] items-center justify-center text-gray-300">
<Loader2Icon className="size-4 animate-spin" />
</span>
)}
</>
)}
{(isSuccess || isBulkUpdateSuccess) && (
<span className="flex size-[30px] items-center justify-center text-green-500">
<CheckIcon additionalClasses="size-4" />
</span>
)}
</div>
);
}

View File

@@ -0,0 +1,60 @@
import { useQuery } from '@tanstack/react-query';
import { getPercentage } from '../../lib/number';
import { userResourceProgressOptions } from '../../queries/resource-progress';
import { queryClient } from '../../stores/query-client';
type RoadmapChatUserProgressListProps = {
roadmapId: string;
};
export function RoadmapChatUserProgressList(
props: RoadmapChatUserProgressListProps,
) {
const { roadmapId } = props;
const { data: userResourceProgressData } = useQuery(
userResourceProgressOptions('roadmap', roadmapId),
queryClient,
);
const doneCount = userResourceProgressData?.done?.length ?? 0;
const skippedCount = userResourceProgressData?.skipped?.length ?? 0;
const totalTopicCount = userResourceProgressData?.totalTopicCount ?? 0;
const totalFinished = doneCount + skippedCount;
const progressPercentage = getPercentage(totalFinished, totalTopicCount);
return (
<div className="relative my-6 flex flex-col gap-3 rounded-xl border border-gray-200 bg-white p-4 first:mt-0 last:mb-0">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-600">Progress</span>
<span className="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-700">
{progressPercentage}%
</span>
</div>
<span className="hidden text-sm font-medium text-gray-600 md:block">
{totalFinished} / {totalTopicCount} topics
</span>
</div>
<div className="relative h-2 w-full overflow-hidden rounded-full bg-gray-100">
<div
className="absolute inset-0 bg-gradient-to-r from-green-500 to-green-600 transition-all duration-300"
style={{ width: `${progressPercentage}%` }}
/>
</div>
<div className="flex items-center gap-4 text-xs text-gray-500">
<div className="flex items-center gap-1">
<div className="h-2 w-2 rounded-full bg-green-500" />
<span>Completed: {doneCount}</span>
</div>
<div className="flex items-center gap-1">
<div className="h-2 w-2 rounded-full bg-gray-400" />
<span>Skipped: {skippedCount}</span>
</div>
</div>
</div>
);
}

View File

@@ -10,9 +10,9 @@ import { DashboardTabButton } from './DashboardTabButton';
import { PersonalDashboard, type BuiltInRoadmap } from './PersonalDashboard';
import { TeamDashboard } from './TeamDashboard';
import type { QuestionGroupType } from '../../lib/question-group';
import type { GuideFileType } from '../../lib/guide';
import type { VideoFileType } from '../../lib/video';
import { cn } from '../../lib/classname';
import type { OfficialGuideDocument } from '../../queries/official-guide';
type DashboardPageProps = {
builtInRoleRoadmaps?: BuiltInRoadmap[];
@@ -20,7 +20,7 @@ type DashboardPageProps = {
builtInBestPractices?: BuiltInRoadmap[];
isTeamPage?: boolean;
questionGroups?: QuestionGroupType[];
guides?: GuideFileType[];
guides?: OfficialGuideDocument[];
videos?: VideoFileType[];
};
@@ -30,7 +30,6 @@ export function DashboardPage(props: DashboardPageProps) {
builtInBestPractices,
builtInSkillRoadmaps,
isTeamPage = false,
questionGroups,
guides,
videos,
} = props;
@@ -132,7 +131,6 @@ export function DashboardPage(props: DashboardPageProps) {
builtInRoleRoadmaps={builtInRoleRoadmaps}
builtInSkillRoadmaps={builtInSkillRoadmaps}
builtInBestPractices={builtInBestPractices}
questionGroups={questionGroups}
guides={guides}
videos={videos}
/>

View File

@@ -3,7 +3,6 @@ import { useEffect, useState } from 'react';
import type { AllowedProfileVisibility } from '../../api/user.ts';
import { useToast } from '../../hooks/use-toast';
import { cn } from '../../lib/classname.ts';
import type { GuideFileType } from '../../lib/guide';
import { httpGet } from '../../lib/http';
import type { QuestionGroupType } from '../../lib/question-group';
import type { AllowedRoadmapRenderer } from '../../lib/roadmap.ts';
@@ -21,6 +20,7 @@ import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions';
import type { UserProgress } from '../TeamProgress/TeamProgressPage';
import { useIsPaidUser } from '../../queries/billing.ts';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal.tsx';
import type { OfficialGuideDocument } from '../../queries/official-guide.ts';
const projectGroups = [
{
@@ -66,7 +66,7 @@ type PersonalDashboardProps = {
builtInSkillRoadmaps?: BuiltInRoadmap[];
builtInBestPractices?: BuiltInRoadmap[];
questionGroups?: QuestionGroupType[];
guides?: GuideFileType[];
guides?: OfficialGuideDocument[];
videos?: VideoFileType[];
};
@@ -193,7 +193,6 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
builtInRoleRoadmaps = [],
builtInBestPractices = [],
builtInSkillRoadmaps = [],
questionGroups = [],
guides = [],
videos = [],
} = props;
@@ -466,40 +465,15 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
</div>
</div>
</div>
<div className="relative mt-12 border-t border-t-[#1e293c] pt-12">
<div className="container">
<h2 className="text-md font-regular absolute -top-[17px] left-4 flex rounded-lg border border-[#1e293c] bg-slate-900 px-3 py-1 text-slate-400 sm:left-1/2 sm:-translate-x-1/2">
Questions
</h2>
<div className="grid grid-cols-1 gap-3 px-2 sm:grid-cols-2 sm:px-0 lg:grid-cols-3">
{questionGroups.map((questionGroup) => {
return (
<HeroRoadmap
percentageDone={0}
key={questionGroup.id}
resourceId={questionGroup.id}
resourceType="roadmap"
resourceTitle={questionGroup.frontmatter.briefTitle}
url={`/questions/${questionGroup.id}`}
allowFavorite={false}
isNew={questionGroup.frontmatter.isNew}
/>
);
})}
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-5 bg-gray-50 px-4 py-5 sm:gap-16 sm:px-0 sm:py-16">
<FeaturedGuideList
heading="Guides"
guides={guides}
questions={questionGroups
.filter((questionGroup) => questionGroup.frontmatter.authorId)
.slice(0, 7)}
guides={guides.slice(0, 15)}
questions={guides
.filter((guide) => guide.roadmapId === 'questions')
.slice(0, 15)}
/>
<FeaturedVideoList heading="Videos" videos={videos} />
</div>

View File

@@ -23,7 +23,12 @@ type EditorRoadmapProps = {
};
export function EditorRoadmap(props: EditorRoadmapProps) {
const { resourceId, resourceType = 'roadmap', dimensions, hasChat = true } = props;
const {
resourceId,
resourceType = 'roadmap',
dimensions,
hasChat = true,
} = props;
const [hasSwitchedRoadmap, setHasSwitchedRoadmap] = useState(false);
const [isLoading, setIsLoading] = useState(true);

View File

@@ -1,3 +0,0 @@
<div class='text-sm sm:text-base leading-relaxed text-left p-2 sm:p-4 text-md text-gray-800 border-t border-t-gray-300 bg-gray-100 rounded-bl-md rounded-br-md [&>p:not(:last-child)]:mb-3 [&>p>a]:underline [&>p>a]:text-blue-700'>
<slot />
</div>

View File

@@ -1,42 +0,0 @@
---
import { markdownToHtml } from '../../lib/markdown';
import Answer from './Answer.astro';
import Question from './Question.astro';
export type FAQType = {
question: string;
answer: string[];
};
export interface Props {
faqs: FAQType[];
}
const { faqs } = Astro.props;
if (faqs.length === 0) {
return '';
}
---
<div class='border-t bg-gray-100 mt-8'>
<div class='container'>
<div class='flex justify-between relative -top-5'>
<h2 class='text-sm sm:text-base font-medium py-1 px-3 border bg-white rounded-md'>Frequently Asked Questions</h2>
</div>
<div class='flex flex-col gap-1 pb-14'>
{
faqs.map((faq, questionIndex) => (
<Question isActive={questionIndex === 0} question={faq.question}>
<Answer>
{faq.answer.map((answer) => (
<p set:html={markdownToHtml(answer)} />
))}
</Answer>
</Question>
))
}
</div>
</div>
</div>

View File

@@ -0,0 +1,61 @@
import { useState } from 'react';
import type { OfficialRoadmapQuestion } from '../../queries/official-roadmap';
import { Question } from './Question';
import { guideRenderer } from '../../lib/guide-renderer';
type FAQsProps = {
faqs: OfficialRoadmapQuestion[];
};
export function FAQs(props: FAQsProps) {
const { faqs } = props;
if (faqs.length === 0) {
return null;
}
const [activeQuestionIndex, setActiveQuestionIndex] = useState(0);
return (
<div className="mt-8 border-t bg-gray-100">
<div className="container">
<div className="relative -top-5 flex justify-between">
<h2 className="rounded-md border bg-white px-3 py-1 text-sm font-medium sm:text-base">
Frequently Asked Questions
</h2>
</div>
<div className="flex flex-col gap-1 pb-14">
{faqs.map((faq, questionIndex) => {
const isTextDescription =
typeof faq?.description === 'string' &&
faq?.description?.length > 0;
return (
<Question
key={faq._id}
isActive={questionIndex === activeQuestionIndex}
question={faq.title}
onClick={() => setActiveQuestionIndex(questionIndex)}
>
<div
className="text-md rounded-br-md rounded-bl-md border-t border-t-gray-300 bg-gray-100 p-2 text-left text-sm leading-relaxed text-gray-800 sm:p-4 sm:text-base [&>p:not(:last-child)]:mb-3 [&>p>a]:text-blue-700 [&>p>a]:underline"
{...(isTextDescription
? {
dangerouslySetInnerHTML: {
__html: faq.description,
},
}
: {})}
>
{!isTextDescription
? guideRenderer.render(faq.description)
: null}
</div>
</Question>
);
})}
</div>
</div>
</div>
);
}

View File

@@ -1,42 +0,0 @@
---
import Icon from '../AstroIcon.astro';
export interface Props {
question: string;
isActive?: boolean;
}
const { question, isActive = false } = Astro.props;
---
<div
class='faq-item bg-white border rounded-md hover:bg-gray-50 border-gray-300'
>
<button
faq-question
class='flex flex-row justify-between items-center p-2 sm:p-3 w-full'
>
<span class='text-sm sm:text-base text-left font-medium'>{question}</span>
<Icon icon='down' class='h-6 hidden sm:block text-gray-400' />
</button>
<div class:list={['answer', { hidden: !isActive }]} faq-answer>
<slot />
</div>
</div>
<script>
document.querySelectorAll('[faq-question]').forEach((el) => {
el.addEventListener('click', () => {
// Hide any other visible answers
document.querySelectorAll('[faq-answer]').forEach((element) => {
element.classList.add('hidden');
});
// Show the current answer
const answer = el.nextElementSibling;
if (answer) {
answer.classList.remove('hidden');
}
});
});
</script>

View File

@@ -0,0 +1,29 @@
import { cn } from '../../lib/classname';
import { ChevronDownIcon } from '../ReactIcons/ChevronDownIcon';
type QuestionProps = {
question: string;
isActive?: boolean;
children: React.ReactNode;
onClick?: () => void;
};
export function Question(props: QuestionProps) {
const { question, isActive = false, children, onClick } = props;
return (
<div className="faq-item rounded-md border border-gray-300 bg-white hover:bg-gray-50">
<button
className="flex w-full flex-row items-center justify-between p-2 sm:p-3"
onClick={onClick}
>
<span className="text-left text-sm font-medium sm:text-base">
{question}
</span>
<ChevronDownIcon className="hidden h-3.5 stroke-[3] text-gray-400 sm:block" />
</button>
{isActive && <div className={cn('answer')}>{children}</div>}
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More