mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2026-03-13 10:11:55 +08:00
Compare commits
46 Commits
devops-con
...
feat/sync-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67530bdbc5 | ||
|
|
29162e9de1 | ||
|
|
fe047b23db | ||
|
|
8841fe1ce8 | ||
|
|
819550f0b0 | ||
|
|
ea0934b0bd | ||
|
|
bbedfec17d | ||
|
|
96b2eb2797 | ||
|
|
fc1f666daf | ||
|
|
8fb38ae944 | ||
|
|
bfe340508c | ||
|
|
fc260ec3f0 | ||
|
|
cd18dbad95 | ||
|
|
949ada2fda | ||
|
|
2823038d79 | ||
|
|
dbb25ca129 | ||
|
|
467581bbf4 | ||
|
|
bd7cf6e4d7 | ||
|
|
12dd62fbeb | ||
|
|
10e179345c | ||
|
|
830d365f3b | ||
|
|
50b04042ee | ||
|
|
e471c8b393 | ||
|
|
a63eb8e934 | ||
|
|
f79d8c0562 | ||
|
|
a024a573fe | ||
|
|
b01adcc62e | ||
|
|
a313552721 | ||
|
|
4931ba060f | ||
|
|
bb47e557c6 | ||
|
|
f0a5853058 | ||
|
|
7072431723 | ||
|
|
79f9e72a9d | ||
|
|
b9502b8256 | ||
|
|
a25dced848 | ||
|
|
0aab2a24b8 | ||
|
|
6894e73781 | ||
|
|
c1d3db0c97 | ||
|
|
dc8ad22192 | ||
|
|
df1cdde166 | ||
|
|
dfb3238097 | ||
|
|
4fcff0c593 | ||
|
|
07b85c032a | ||
|
|
20c1a54198 | ||
|
|
0a4d6871db | ||
|
|
aeda7a369c |
@@ -3,6 +3,6 @@
|
||||
"enabled": false
|
||||
},
|
||||
"_variables": {
|
||||
"lastUpdateCheck": 1753810743067
|
||||
"lastUpdateCheck": 1755042938009
|
||||
}
|
||||
}
|
||||
1
.astro/types.d.ts
vendored
1
.astro/types.d.ts
vendored
@@ -1,2 +1 @@
|
||||
/// <reference types="astro/client" />
|
||||
/// <reference path="content.d.ts" />
|
||||
67
.github/workflows/sync-content-to-repo.yml
vendored
Normal file
67
.github/workflows/sync-content-to-repo.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
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: |
|
||||
dependencies
|
||||
automated pr
|
||||
reviewers: arikchakma
|
||||
commit-message: "chore: sync content to repo"
|
||||
title: "Sync Content to Repo - Automated"
|
||||
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.**
|
||||
66
.github/workflows/sync-repo-to-database.yml
vendored
Normal file
66
.github/workflows/sync-repo-to-database.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
name: Sync on Roadmap Changes
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'src/data/roadmaps/**'
|
||||
|
||||
jobs:
|
||||
sync-on-changes:
|
||||
runs-on: ubuntu-latest
|
||||
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.TOPIC_CONTENT_SYNC_SECRET }}
|
||||
12
package.json
12
package.json
@@ -29,9 +29,12 @@
|
||||
"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",
|
||||
"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 +46,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 +69,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 +85,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 +107,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 +123,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
474
pnpm-lock.yaml
generated
@@ -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)
|
||||
|
||||
BIN
public/pdfs/roadmaps/data-engineer.pdf
Normal file
BIN
public/pdfs/roadmaps/data-engineer.pdf
Normal file
Binary file not shown.
BIN
public/pdfs/roadmaps/machine-learning.pdf
Normal file
BIN
public/pdfs/roadmaps/machine-learning.pdf
Normal file
Binary file not shown.
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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 system’s business value against the cost and risk of maintaining it.",
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
{
|
||||
|
||||
BIN
public/roadmaps/data-engineer.png
Normal file
BIN
public/roadmaps/data-engineer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 583 KiB |
BIN
public/roadmaps/machine-learning.png
Normal file
BIN
public/roadmaps/machine-learning.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 523 KiB |
@@ -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,8 @@ 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)
|
||||
- [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)
|
||||
|
||||
110
scripts/sync-content-to-repo.ts
Normal file
110
scripts/sync-content-to-repo.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { slugify } from '../src/lib/slugger';
|
||||
|
||||
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 const allowedOfficialRoadmapTopicResourceType = [
|
||||
'roadmap',
|
||||
'official',
|
||||
'opensource',
|
||||
'article',
|
||||
'course',
|
||||
'podcast',
|
||||
'video',
|
||||
'book',
|
||||
'feed',
|
||||
] as const;
|
||||
export type AllowedOfficialRoadmapTopicResourceType =
|
||||
(typeof allowedOfficialRoadmapTopicResourceType)[number];
|
||||
|
||||
export type OfficialRoadmapTopicResource = {
|
||||
_id?: string;
|
||||
type: AllowedOfficialRoadmapTopicResourceType;
|
||||
title: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export interface OfficialRoadmapTopicContentDocument {
|
||||
_id?: string;
|
||||
roadmapSlug: string;
|
||||
nodeId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
resources: OfficialRoadmapTopicResource[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Directory containing the roadmaps
|
||||
const ROADMAP_CONTENT_DIR = path.join(
|
||||
__dirname,
|
||||
'../src/data/roadmaps',
|
||||
roadmapSlug,
|
||||
);
|
||||
|
||||
const allTopics = await roadmapTopics(roadmapSlug, secret);
|
||||
for (const topic of allTopics) {
|
||||
const { title, nodeId } = topic;
|
||||
|
||||
const topicSlug = `${slugify(title)}@${nodeId}.md`;
|
||||
|
||||
const topicPath = path.join(ROADMAP_CONTENT_DIR, 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;
|
||||
|
||||
const content = `${description}
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
${resources.map((resource) => `- [@${resource.type}@${resource.title}](${resource.url})`).join('\n')}
|
||||
`.trim();
|
||||
|
||||
return content;
|
||||
}
|
||||
218
scripts/sync-repo-to-database.ts
Normal file
218
scripts/sync-repo-to-database.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
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 { parse } from 'node-html-parser';
|
||||
import { markdownToHtml } from '../src/lib/markdown';
|
||||
import { htmlToMarkdown } from '../src/lib/html';
|
||||
import {
|
||||
allowedOfficialRoadmapTopicResourceType,
|
||||
type AllowedOfficialRoadmapTopicResourceType,
|
||||
type OfficialRoadmapTopicContentDocument,
|
||||
type OfficialRoadmapTopicResource,
|
||||
} from './sync-content-to-repo';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const allFiles = args?.[0]?.replace('--files=', '');
|
||||
const secret = args?.[1]?.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}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.error) {
|
||||
throw new Error(`Failed to fetch roadmap json: ${data.error}`);
|
||||
}
|
||||
|
||||
roadmapJsonCache.set(roadmapId, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function syncContentToDatabase(
|
||||
topics: Omit<
|
||||
OfficialRoadmapTopicContentDocument,
|
||||
'createdAt' | 'updatedAt' | '_id'
|
||||
>[],
|
||||
) {
|
||||
const response = await fetch(
|
||||
`https://roadmap.sh/api/v1-sync-official-roadmap-topics`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
topics,
|
||||
secret,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to sync content to database: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
const files = allFiles.split(' ');
|
||||
console.log(`🚀 Starting ${files.length} files`);
|
||||
|
||||
const ROADMAP_CONTENT_DIR = path.join(__dirname, '../src/data/roadmaps');
|
||||
|
||||
try {
|
||||
const topics: Omit<
|
||||
OfficialRoadmapTopicContentDocument,
|
||||
'createdAt' | 'updatedAt' | '_id'
|
||||
>[] = [];
|
||||
|
||||
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 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: Omit<OfficialRoadmapTopicResource, '_id'>[] =
|
||||
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}
|
||||
|
||||
${description}`.trim();
|
||||
|
||||
const label = node?.data?.label as string;
|
||||
if (!label) {
|
||||
console.error(`🚨 Label is required: ${file}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
topics.push({
|
||||
roadmapSlug,
|
||||
nodeId,
|
||||
title: label,
|
||||
description: updatedDescription,
|
||||
resources: listLinks,
|
||||
});
|
||||
}
|
||||
|
||||
await syncContentToDatabase(topics);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
131
src/components/ChatMessages/AIChat.css
Normal file
131
src/components/ChatMessages/AIChat.css
Normal 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;
|
||||
}
|
||||
}
|
||||
108
src/components/ChatMessages/RoadmapChatIntroMessage.tsx
Normal file
108
src/components/ChatMessages/RoadmapChatIntroMessage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
148
src/components/ChatMessages/RoadmapChatMessage.tsx
Normal file
148
src/components/ChatMessages/RoadmapChatMessage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
110
src/components/ChatMessages/RoadmapChatMessages.tsx
Normal file
110
src/components/ChatMessages/RoadmapChatMessages.tsx
Normal 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);
|
||||
82
src/components/ChatMessages/RoadmapRecommendations.tsx
Normal file
82
src/components/ChatMessages/RoadmapRecommendations.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
106
src/components/ChatMessages/RoadmapTopicList.tsx
Normal file
106
src/components/ChatMessages/RoadmapTopicList.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
47
src/components/ChatMessages/ShareResourceLink.tsx
Normal file
47
src/components/ChatMessages/ShareResourceLink.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
63
src/components/ChatMessages/TopicChatMessage.tsx
Normal file
63
src/components/ChatMessages/TopicChatMessage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
57
src/components/ChatMessages/TopicChatMessages.tsx
Normal file
57
src/components/ChatMessages/TopicChatMessages.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
330
src/components/ChatMessages/UserPrgressActionList.tsx
Normal file
330
src/components/ChatMessages/UserPrgressActionList.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
60
src/components/ChatMessages/UserProgressList.tsx
Normal file
60
src/components/ChatMessages/UserProgressList.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import type { GuideFileType } from '../../lib/guide';
|
||||
import type { QuestionGroupType } from '../../lib/question-group';
|
||||
import type { OfficialGuideDocument } from '../../queries/official-guide';
|
||||
import { GuideListItem } from './GuideListItem';
|
||||
|
||||
export interface FeaturedGuidesProps {
|
||||
heading: string;
|
||||
guides: GuideFileType[];
|
||||
questions: QuestionGroupType[];
|
||||
guides: OfficialGuideDocument[];
|
||||
questions: OfficialGuideDocument[];
|
||||
}
|
||||
|
||||
export function FeaturedGuideList(props: FeaturedGuidesProps) {
|
||||
const { heading, guides, questions = [] } = props;
|
||||
|
||||
const sortedGuides: (QuestionGroupType | GuideFileType)[] = [
|
||||
...guides,
|
||||
...questions,
|
||||
].sort((a, b) => {
|
||||
const aDate = new Date(a.frontmatter.date as string);
|
||||
const bDate = new Date(b.frontmatter.date as string);
|
||||
const sortedGuides = [...guides, ...questions].sort((a, b) => {
|
||||
const aDate = new Date(a.publishedAt ?? new Date());
|
||||
const bDate = new Date(b.publishedAt ?? new Date());
|
||||
|
||||
return bDate.getTime() - aDate.getTime();
|
||||
});
|
||||
@@ -27,7 +23,7 @@ export function FeaturedGuideList(props: FeaturedGuidesProps) {
|
||||
|
||||
<div className="mt-3 sm:my-5">
|
||||
{sortedGuides.map((guide) => (
|
||||
<GuideListItem key={guide.id} guide={guide} />
|
||||
<GuideListItem key={guide._id} guide={guide} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -48,4 +44,4 @@ export function FeaturedGuideList(props: FeaturedGuidesProps) {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +1,46 @@
|
||||
import type { GuideFileType, GuideFrontmatter } from '../../lib/guide';
|
||||
import { type QuestionGroupType } from '../../lib/question-group';
|
||||
import dayjs from 'dayjs';
|
||||
import { DateTime } from 'luxon';
|
||||
import {
|
||||
getOfficialGuideHref,
|
||||
type OfficialGuideDocument,
|
||||
} from '../../queries/official-guide';
|
||||
|
||||
export interface GuideListItemProps {
|
||||
guide: GuideFileType | QuestionGroupType;
|
||||
}
|
||||
|
||||
function isQuestionGroupType(
|
||||
guide: GuideFileType | QuestionGroupType,
|
||||
): guide is QuestionGroupType {
|
||||
return (guide as QuestionGroupType).questions !== undefined;
|
||||
guide: OfficialGuideDocument;
|
||||
}
|
||||
|
||||
export function GuideListItem(props: GuideListItemProps) {
|
||||
const { guide } = props;
|
||||
const { frontmatter, id } = guide;
|
||||
const { title, slug, publishedAt, roadmapId } = guide;
|
||||
|
||||
let pageUrl = '';
|
||||
let guideType = '';
|
||||
|
||||
if (isQuestionGroupType(guide)) {
|
||||
pageUrl = `/questions/${id}`;
|
||||
guideType = 'Questions';
|
||||
} else {
|
||||
const excludedBySlug = (frontmatter as GuideFrontmatter).excludedBySlug;
|
||||
pageUrl = excludedBySlug ? excludedBySlug : `/guides/${id}`;
|
||||
guideType = (frontmatter as GuideFrontmatter).type;
|
||||
let guideType = 'Textual';
|
||||
if (roadmapId === 'questions') {
|
||||
guideType = 'Question';
|
||||
}
|
||||
|
||||
// Check if article is within the last 15 days
|
||||
const isNew = frontmatter.date
|
||||
? dayjs().diff(dayjs(frontmatter.date), 'day') < 15
|
||||
: false;
|
||||
const publishedAtDate = publishedAt
|
||||
? DateTime.fromJSDate(new Date(publishedAt))
|
||||
: null;
|
||||
|
||||
const isNew =
|
||||
publishedAtDate && DateTime.now().diff(publishedAtDate, 'days').days < 15;
|
||||
const publishedAtMonth = publishedAtDate
|
||||
? publishedAtDate.toFormat('MMMM')
|
||||
: '';
|
||||
|
||||
return (
|
||||
<a
|
||||
className="text-md group block flex items-center justify-between border-b py-2 text-gray-600 no-underline hover:text-blue-600"
|
||||
href={pageUrl}
|
||||
className="text-md group flex items-center justify-between border-b py-2 text-gray-600 no-underline hover:text-blue-600"
|
||||
href={getOfficialGuideHref(slug, roadmapId)}
|
||||
>
|
||||
<span className="text-sm transition-transform group-hover:translate-x-2 md:text-base">
|
||||
{frontmatter.title}
|
||||
{title}
|
||||
|
||||
{isNew && (
|
||||
<span className="ml-2.5 rounded-xs bg-green-300 px-1.5 py-0.5 text-xs font-medium text-green-900 uppercase">
|
||||
New
|
||||
<span className="hidden sm:inline">
|
||||
·
|
||||
{frontmatter.date ? dayjs(frontmatter.date).format('MMMM') : ''}
|
||||
{publishedAtMonth}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import '../ChatMessages/AIChat.css';
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { JSONContent } from '@tiptap/core';
|
||||
import {
|
||||
@@ -14,13 +16,9 @@ import {
|
||||
Wand2,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { useKeydown } from '../../hooks/use-keydown';
|
||||
import {
|
||||
roadmapAIChatRenderer,
|
||||
useRoadmapAIChat,
|
||||
} from '../../hooks/use-roadmap-ai-chat';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { lockBodyScroll } from '../../lib/dom';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
@@ -33,10 +31,14 @@ import { roadmapJSONOptions } from '../../queries/roadmap';
|
||||
import { roadmapQuestionsOptions } from '../../queries/roadmap-questions';
|
||||
import { queryClient } from '../../stores/query-client';
|
||||
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
|
||||
import { RoadmapAIChatCard } from '../RoadmapAIChat/RoadmapAIChatCard';
|
||||
import { RoadmapAIChatHistory } from '../RoadmapAIChatHistory/RoadmapAIChatHistory';
|
||||
import { CLOSE_TOPIC_DETAIL_EVENT } from '../TopicDetail/TopicDetail';
|
||||
import { UpdatePersonaModal } from '../UserPersona/UpdatePersonaModal';
|
||||
import { shuffle } from '../../helper/shuffle';
|
||||
import { useChat } from '@ai-sdk/react';
|
||||
import { chatRoadmapTransport } from '../../lib/ai';
|
||||
import { useAIChatScroll } from '../../hooks/use-ai-chat-scroll';
|
||||
import { RoadmapChatMessages } from '../ChatMessages/RoadmapChatMessages';
|
||||
|
||||
type ChatHeaderButtonProps = {
|
||||
onClick?: () => void;
|
||||
@@ -158,10 +160,12 @@ type RoadmapChatProps = {
|
||||
|
||||
export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
||||
const { roadmapId } = props;
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const scrollareaRef = useRef<HTMLDivElement>(null);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [isPersonalizeOpen, setIsPersonalizeOpen] = useState(false);
|
||||
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
|
||||
|
||||
@@ -176,9 +180,7 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
||||
if (!questionsData?.questions || questionsData.questions.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const shuffled = [...questionsData.questions].sort(
|
||||
() => 0.5 - Math.random(),
|
||||
);
|
||||
const shuffled = shuffle([...questionsData.questions]);
|
||||
return shuffled.slice(0, 4);
|
||||
}, [questionsData]);
|
||||
|
||||
@@ -236,45 +238,36 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
||||
string | undefined
|
||||
>();
|
||||
const { data: chatHistory } = useQuery(
|
||||
chatHistoryOptions(
|
||||
activeChatHistoryId,
|
||||
roadmapAIChatRenderer({
|
||||
roadmapId,
|
||||
totalTopicCount,
|
||||
onSelectTopic,
|
||||
}),
|
||||
),
|
||||
chatHistoryOptions(activeChatHistoryId),
|
||||
queryClient,
|
||||
);
|
||||
|
||||
const {
|
||||
aiChatHistory,
|
||||
isStreamingMessage,
|
||||
streamedMessage,
|
||||
showScrollToBottom,
|
||||
setShowScrollToBottom,
|
||||
handleChatSubmit,
|
||||
handleAbort,
|
||||
scrollToBottom,
|
||||
clearChat,
|
||||
setAiChatHistory,
|
||||
} = useRoadmapAIChat({
|
||||
activeChatHistoryId,
|
||||
roadmapId,
|
||||
totalTopicCount,
|
||||
scrollareaRef,
|
||||
onSelectTopic,
|
||||
onChatHistoryIdChange: (chatHistoryId) => {
|
||||
setActiveChatHistoryId(chatHistoryId);
|
||||
const { messages, sendMessage, status, stop, setMessages } = useChat({
|
||||
transport: chatRoadmapTransport,
|
||||
onData: (data) => {
|
||||
if (data.type === 'data-redirect') {
|
||||
const { title, chatId } = data.data as {
|
||||
title: string;
|
||||
chatId: string;
|
||||
};
|
||||
|
||||
document.title = title;
|
||||
setActiveChatHistoryId(chatId);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { scrollToBottom, scrollableContainerRef, showScrollToBottomButton } =
|
||||
useAIChatScroll({
|
||||
messages,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!chatHistory) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAiChatHistory(chatHistory?.messages ?? []);
|
||||
setMessages(chatHistory?.messages ?? []);
|
||||
setIsChatHistoryLoading(false);
|
||||
setTimeout(() => {
|
||||
scrollToBottom('instant');
|
||||
@@ -286,9 +279,9 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAiChatHistory([]);
|
||||
setMessages([]);
|
||||
setIsChatHistoryLoading(false);
|
||||
}, [activeChatHistoryId, setAiChatHistory, setIsChatHistoryLoading]);
|
||||
}, [activeChatHistoryId]);
|
||||
|
||||
useEffect(() => {
|
||||
lockBodyScroll(isOpen);
|
||||
@@ -320,26 +313,45 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
||||
};
|
||||
}
|
||||
|
||||
const submitInput = () => {
|
||||
const clearChat = () => {
|
||||
setMessages([]);
|
||||
setInputValue('');
|
||||
};
|
||||
|
||||
const submitInput = (message?: string) => {
|
||||
if (!isLoggedIn()) {
|
||||
setIsOpen(false);
|
||||
showLoginPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
const trimmed = inputValue.trim();
|
||||
const trimmed = (message ?? inputValue).trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const json: JSONContent = textToJSON(trimmed);
|
||||
sendMessage(
|
||||
{ text: trimmed, metadata: { json: textToJSON(trimmed) } },
|
||||
{
|
||||
body: {
|
||||
roadmapId,
|
||||
...(activeChatHistoryId
|
||||
? { chatHistoryId: activeChatHistoryId }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
setInputValue('');
|
||||
handleChatSubmit(json, isRoadmapDetailLoading);
|
||||
setTimeout(() => {
|
||||
scrollToBottom('smooth');
|
||||
setInputValue('');
|
||||
inputRef.current?.focus();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const hasMessages = aiChatHistory.length > 0;
|
||||
const newTabUrl = `/${roadmapId}/ai${activeChatHistoryId ? `?chatId=${activeChatHistoryId}` : ''}`;
|
||||
const isStreamingMessage = status !== 'ready';
|
||||
const hasMessages = messages.length > 0;
|
||||
const newTabUrl = `/ai/roadmap-chat/${roadmapId}${activeChatHistoryId ? `?chatId=${activeChatHistoryId}` : ''}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -371,7 +383,7 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'animate-fade-slide-up fixed bottom-5 left-1/2 max-h-[95vh] max-w-[968px] -translate-x-1/4 transform flex-col gap-1.5 overflow-hidden px-4 duration-300 sm:max-h-[50vh] lg:flex',
|
||||
'animate-fade-slide-up ai-chat fixed bottom-5 left-1/2 max-h-[95vh] max-w-[968px] -translate-x-1/4 transform flex-col gap-1.5 overflow-hidden px-4 duration-300 sm:max-h-[50vh] lg:flex',
|
||||
isOpen ? 'z-91 h-full w-full' : 'z-40 w-auto',
|
||||
)}
|
||||
>
|
||||
@@ -417,7 +429,6 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
||||
onChatHistoryClick={(chatHistoryId) => {
|
||||
setIsChatHistoryLoading(true);
|
||||
setActiveChatHistoryId(chatHistoryId);
|
||||
setShowScrollToBottom(false);
|
||||
}}
|
||||
onDelete={(chatHistoryId) => {
|
||||
if (activeChatHistoryId === chatHistoryId) {
|
||||
@@ -443,82 +454,27 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-1 flex-grow flex-col overflow-y-auto px-3 py-2"
|
||||
ref={scrollareaRef}
|
||||
>
|
||||
<div className="flex flex-col gap-2 text-sm">
|
||||
<RoadmapAIChatCard
|
||||
role="assistant"
|
||||
jsx={
|
||||
<span className="mt-[2px]">
|
||||
Hey, I am your AI tutor. How can I help you today? 👋
|
||||
</span>
|
||||
}
|
||||
isIntro
|
||||
<div className="relative flex grow flex-col">
|
||||
<div
|
||||
className="relative grow overflow-y-auto"
|
||||
ref={scrollableContainerRef}
|
||||
>
|
||||
<RoadmapChatMessages
|
||||
messages={messages}
|
||||
status={status}
|
||||
roadmapId={roadmapId}
|
||||
defaultQuestions={defaultQuestions}
|
||||
onTopicClick={onSelectTopic}
|
||||
onDefaultQuestionClick={submitInput}
|
||||
/>
|
||||
|
||||
{/* Show default questions only when there's no chat history */}
|
||||
{aiChatHistory.length === 0 &&
|
||||
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={() => {
|
||||
if (!isLoggedIn()) {
|
||||
setIsOpen(false);
|
||||
showLoginPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLimitExceeded) {
|
||||
setShowUpgradeModal(true);
|
||||
setIsOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
handleChatSubmit(
|
||||
textToJSON(question),
|
||||
isRoadmapDetailLoading,
|
||||
);
|
||||
}}
|
||||
>
|
||||
{question}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{aiChatHistory.map((chat, index) => (
|
||||
<Fragment key={`chat-${index}`}>
|
||||
<RoadmapAIChatCard {...chat} />
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
{isStreamingMessage && !streamedMessage && (
|
||||
<RoadmapAIChatCard role="assistant" html="Thinking..." />
|
||||
)}
|
||||
|
||||
{streamedMessage && (
|
||||
<RoadmapAIChatCard role="assistant" jsx={streamedMessage} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Scroll to bottom button */}
|
||||
{showScrollToBottom && (
|
||||
{showScrollToBottomButton && (
|
||||
<button
|
||||
onClick={() => {
|
||||
scrollToBottom('instant');
|
||||
setShowScrollToBottom(false);
|
||||
}}
|
||||
className="sticky bottom-0 mx-auto mt-2 flex items-center gap-1.5 rounded-full bg-gray-900 px-3 py-1.5 text-xs text-white shadow-lg transition-all hover:bg-gray-800"
|
||||
className="absolute inset-x-0 bottom-2 mx-auto mt-2 flex w-fit items-center gap-1.5 rounded-full bg-gray-900 px-3 py-1.5 text-xs text-white shadow-lg transition-all hover:bg-gray-800"
|
||||
>
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
Scroll to bottom
|
||||
@@ -534,6 +490,7 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isLimitExceeded && (
|
||||
<>
|
||||
<div className="flex flex-row justify-between border-t border-gray-200 px-3 pt-2">
|
||||
@@ -587,9 +544,10 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (isStreamingMessage) {
|
||||
if (status !== 'ready') {
|
||||
return;
|
||||
}
|
||||
|
||||
submitInput();
|
||||
}
|
||||
}}
|
||||
@@ -609,9 +567,10 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
||||
disabled={isRoadmapDetailLoading || isLimitExceeded}
|
||||
onClick={() => {
|
||||
if (isStreamingMessage) {
|
||||
handleAbort();
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
submitInput();
|
||||
}}
|
||||
>
|
||||
@@ -637,7 +596,6 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
||||
setIsOpen(true);
|
||||
setTimeout(() => {
|
||||
scrollToBottom('instant');
|
||||
setShowScrollToBottom(false);
|
||||
}, 0);
|
||||
}}
|
||||
>
|
||||
|
||||
160
src/components/Global/CodeBlock.tsx
Normal file
160
src/components/Global/CodeBlock.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import {
|
||||
transformerNotationDiff,
|
||||
transformerNotationErrorLevel,
|
||||
transformerNotationFocus,
|
||||
transformerNotationHighlight,
|
||||
transformerNotationWordHighlight,
|
||||
} from '@shikijs/transformers';
|
||||
import { CheckIcon, CopyIcon } from 'lucide-react';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
import { type BundledLanguage, codeToHtml } from 'shiki';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { useCopyText } from '../../hooks/use-copy-text';
|
||||
export type { BundledLanguage } from 'shiki';
|
||||
|
||||
const codeBlockClassName = cn(
|
||||
'mt-0 text-sm',
|
||||
'[&_pre]:py-0',
|
||||
'[&_pre]:grid',
|
||||
'[&_code]:py-4',
|
||||
'[&_code]:w-full',
|
||||
'[&_code]:grid',
|
||||
'[&_code]:overflow-x-auto',
|
||||
'[&_code]:no-scrollbar',
|
||||
'[&_code]:bg-transparent',
|
||||
'[&_.line]:px-3',
|
||||
'[&_.line]:w-full',
|
||||
'[&_.line]:relative',
|
||||
'[&_.line]:min-h-5',
|
||||
);
|
||||
|
||||
function highlight(html: string, language?: BundledLanguage) {
|
||||
return codeToHtml(html, {
|
||||
lang: language ?? 'typescript',
|
||||
theme: 'github-light',
|
||||
transformers: [
|
||||
transformerNotationDiff({
|
||||
matchAlgorithm: 'v3',
|
||||
}),
|
||||
transformerNotationHighlight({
|
||||
matchAlgorithm: 'v3',
|
||||
}),
|
||||
transformerNotationWordHighlight({
|
||||
matchAlgorithm: 'v3',
|
||||
}),
|
||||
transformerNotationFocus({
|
||||
matchAlgorithm: 'v3',
|
||||
}),
|
||||
transformerNotationErrorLevel({
|
||||
matchAlgorithm: 'v3',
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
type CodeBlockFallbackProps = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
const CodeBlockFallback = ({ children, ...props }: CodeBlockFallbackProps) => (
|
||||
<div {...props}>
|
||||
<pre className="w-full bg-white">
|
||||
<code>
|
||||
{children
|
||||
?.toString()
|
||||
.split('\n')
|
||||
.map((line, i) => (
|
||||
<span className="line" key={i}>
|
||||
{line}
|
||||
</span>
|
||||
))}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
|
||||
export type CodeBlockItemProps = HTMLAttributes<HTMLDivElement> & {
|
||||
value: string;
|
||||
lineNumbers?: boolean;
|
||||
};
|
||||
|
||||
export const CodeBlockItem = ({
|
||||
children,
|
||||
lineNumbers = true,
|
||||
className,
|
||||
value,
|
||||
...props
|
||||
}: CodeBlockItemProps) => {
|
||||
return (
|
||||
<div className={cn(codeBlockClassName, className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & {
|
||||
language?: BundledLanguage;
|
||||
syntaxHighlighting?: boolean;
|
||||
children: string;
|
||||
};
|
||||
|
||||
export const CodeBlockContent = ({
|
||||
children,
|
||||
language,
|
||||
syntaxHighlighting = true,
|
||||
...props
|
||||
}: CodeBlockContentProps) => {
|
||||
const [html, setHtml] = useState<string | null>(null);
|
||||
useLayoutEffect(() => {
|
||||
if (!syntaxHighlighting) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof children !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
highlight(children, language).then(setHtml).catch(console.error);
|
||||
}, [children, syntaxHighlighting, language]);
|
||||
|
||||
if (!(syntaxHighlighting && html)) {
|
||||
return <CodeBlockFallback>{children}</CodeBlockFallback>;
|
||||
}
|
||||
|
||||
return <div dangerouslySetInnerHTML={{ __html: html }} {...props} />;
|
||||
};
|
||||
|
||||
type CodeBlockHeaderProps = HTMLAttributes<HTMLDivElement> & {
|
||||
language: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
export function CodeBlockHeader(props: CodeBlockHeaderProps) {
|
||||
const { language, code, className, ...rest } = props;
|
||||
|
||||
const { copyText, isCopied } = useCopyText();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-between gap-2 border-b border-gray-200 bg-gray-50 px-3 py-2',
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<span className="text-sm text-gray-600">{language}</span>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => copyText(code)}
|
||||
className="flex size-6 items-center justify-center gap-2 rounded-md text-gray-400 hover:bg-zinc-200 hover:text-black focus:outline-none"
|
||||
>
|
||||
{isCopied ? (
|
||||
<CheckIcon className="size-3.5" />
|
||||
) : (
|
||||
<CopyIcon className="size-3.5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
99
src/components/Global/Markdown.tsx
Normal file
99
src/components/Global/Markdown.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import 'katex/dist/katex.min.css';
|
||||
|
||||
import { memo } from 'react';
|
||||
import ReactMarkdown, { type Options } from 'react-markdown';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkMath from 'remark-math';
|
||||
import { cn } from '../../lib/classname';
|
||||
import {
|
||||
CodeBlockContent,
|
||||
CodeBlockHeader,
|
||||
CodeBlockItem,
|
||||
type BundledLanguage,
|
||||
} from './CodeBlock';
|
||||
|
||||
function getLanguage(children: React.ReactNode) {
|
||||
if (
|
||||
typeof children === 'object' &&
|
||||
children !== null &&
|
||||
'type' in children &&
|
||||
children.type === 'code' &&
|
||||
'props' in children &&
|
||||
typeof children.props === 'object' &&
|
||||
children.props !== null &&
|
||||
'className' in children.props &&
|
||||
typeof children.props.className === 'string'
|
||||
) {
|
||||
return children.props.className.replace('language-', '').trim();
|
||||
}
|
||||
|
||||
return 'javascript';
|
||||
}
|
||||
|
||||
const components: Options['components'] = {
|
||||
pre: (props) => {
|
||||
const { children } = props;
|
||||
|
||||
const language = getLanguage(children);
|
||||
const childrenIsCode =
|
||||
typeof children === 'object' &&
|
||||
children !== null &&
|
||||
'type' in children &&
|
||||
children.type === 'code';
|
||||
if (!childrenIsCode) {
|
||||
return <pre>{children}</pre>;
|
||||
}
|
||||
|
||||
// it's fine to do it, because we only have one code block in the markdown
|
||||
// so no worries, it will be fine
|
||||
// we need to remove the last line because it always add a empty line at the end
|
||||
// @see https://github.com/shikijs/shiki/pull/585
|
||||
const code = (children.props as { children: string })?.children?.slice(
|
||||
0,
|
||||
-1
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="not-prose my-6 max-w-full overflow-hidden rounded-lg border border-gray-200">
|
||||
<CodeBlockHeader language={language} code={code} />
|
||||
|
||||
<CodeBlockItem key={language} value={language} lineNumbers={false}>
|
||||
<CodeBlockContent language={language as BundledLanguage}>
|
||||
{code}
|
||||
</CodeBlockContent>
|
||||
</CodeBlockItem>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
type MarkdownProps = {
|
||||
children: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function _Markdown(props: MarkdownProps) {
|
||||
const { children, className } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'overflow-hidden [&>*:first-child]:mt-0 [&>*:last-child]:mb-0',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<ReactMarkdown
|
||||
components={components}
|
||||
rehypePlugins={[rehypeKatex]}
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
>
|
||||
{children}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Markdown = memo(_Markdown, (prevProps, nextProps) => {
|
||||
return prevProps.children === nextProps.children;
|
||||
});
|
||||
@@ -1,72 +0,0 @@
|
||||
---
|
||||
import { getGuideTableOfContent, type GuideFileType } from '../../lib/guide';
|
||||
import MarkdownFile from '../MarkdownFile.astro';
|
||||
import { TableOfContent } from '../TableOfContent/TableOfContent';
|
||||
import { RelatedGuides } from './RelatedGuides';
|
||||
|
||||
interface Props {
|
||||
guide: GuideFileType;
|
||||
}
|
||||
|
||||
const { guide } = Astro.props;
|
||||
|
||||
const allHeadings = guide.getHeadings();
|
||||
const tableOfContent = getGuideTableOfContent(allHeadings);
|
||||
|
||||
const showTableOfContent = tableOfContent.length > 0;
|
||||
const showRelatedGuides =
|
||||
guide?.relatedGuides && Object.keys(guide?.relatedGuides).length > 0;
|
||||
const { frontmatter: guideFrontmatter, author } = guide;
|
||||
---
|
||||
|
||||
<article class='lg:grid lg:max-w-full lg:grid-cols-[1fr_minmax(0,700px)_1fr]'>
|
||||
{
|
||||
(showTableOfContent || showRelatedGuides) && (
|
||||
<div class='sticky top-0 lg:relative bg-linear-to-r from-gray-50 py-0 lg:col-start-3 lg:col-end-4 lg:row-start-1'>
|
||||
<RelatedGuides
|
||||
relatedTitle={guideFrontmatter?.relatedTitle}
|
||||
relatedGuides={guide?.relatedGuides || {}}
|
||||
client:load
|
||||
/>
|
||||
<TableOfContent toc={tableOfContent} client:load />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div
|
||||
class:list={[
|
||||
'col-start-2 col-end-3 row-start-1 mx-auto max-w-[700px] py-5 sm:py-10',
|
||||
{
|
||||
'lg:border-r': showTableOfContent,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<MarkdownFile>
|
||||
<h1 class='mb-3 text-balance text-4xl font-bold'>
|
||||
{guideFrontmatter.title}
|
||||
</h1>
|
||||
<p class='my-0 flex items-center justify-start text-sm text-gray-400'>
|
||||
<a
|
||||
href={`/authors/${author.id}`}
|
||||
class='inline-flex items-center font-medium underline-offset-2 hover:text-gray-600 hover:underline'
|
||||
>
|
||||
<img
|
||||
alt={author.frontmatter.name}
|
||||
src={author.frontmatter.imageUrl}
|
||||
class='mb-0 mr-2 inline h-5 w-5 rounded-full'
|
||||
/>
|
||||
{author.frontmatter.name}
|
||||
</a>
|
||||
<span class='mx-2 hidden sm:inline'>·</span>
|
||||
<a
|
||||
class='hidden underline-offset-2 hover:text-gray-600 sm:inline'
|
||||
href={`https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/guides/${guide.id}.md`}
|
||||
target='_blank'
|
||||
>
|
||||
Improve this Guide
|
||||
</a>
|
||||
</p>
|
||||
<guide.Content />
|
||||
</MarkdownFile>
|
||||
</div>
|
||||
</article>
|
||||
60
src/components/Guide/GuideContent.tsx
Normal file
60
src/components/Guide/GuideContent.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { cn } from '../../lib/classname';
|
||||
import { guideRenderer } from '../../lib/guide-renderer';
|
||||
import type { OfficialGuideResponse } from '../../queries/official-guide';
|
||||
import { TableOfContent } from '../TableOfContent/TableOfContent';
|
||||
import { RelatedGuides } from './RelatedGuides';
|
||||
|
||||
type GuideContentProps = {
|
||||
guide: OfficialGuideResponse;
|
||||
};
|
||||
|
||||
export function GuideContent(props: GuideContentProps) {
|
||||
const { guide } = props;
|
||||
const content = guideRenderer.render(guide.content);
|
||||
const tableOfContents = guideRenderer.tableOfContents(guide.content);
|
||||
const showTableOfContent = tableOfContents.length > 0;
|
||||
const hasRelatedGuides =
|
||||
guide.relatedGuides && guide.relatedGuides.length > 0;
|
||||
|
||||
return (
|
||||
<article className="lg:grid lg:max-w-full lg:grid-cols-[1fr_minmax(0,700px)_1fr]">
|
||||
{(showTableOfContent || hasRelatedGuides) && (
|
||||
<div className="sticky top-0 bg-linear-to-r from-gray-50 py-0 lg:relative lg:col-start-3 lg:col-end-4 lg:row-start-1">
|
||||
{hasRelatedGuides && (
|
||||
<RelatedGuides relatedGuides={guide?.relatedGuides || []} />
|
||||
)}
|
||||
|
||||
{showTableOfContent && <TableOfContent toc={tableOfContents} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'col-start-2 col-end-3 row-start-1 mx-auto max-w-[700px] py-5 sm:py-10',
|
||||
showTableOfContent && 'lg:border-r',
|
||||
)}
|
||||
>
|
||||
<div className="prose prose-xl prose-h2:mb-3 prose-h2:mt-10 prose-h2:scroll-mt-5 prose-h2:text-balance prose-h2:text-3xl prose-h3:mt-2 prose-h4:text-2xl prose-h3:scroll-mt-5 prose-h3:text-balance prose-h4:text-balance prose-h5:text-balance prose-h5:font-medium prose-blockquote:font-normal prose-code:bg-transparent prose-img:mt-1 sm:prose-h2:scroll-mt-10 sm:prose-h3:scroll-mt-10 prose-li:[&>p]:m-0 container">
|
||||
<h1 className="mb-3 text-4xl font-bold text-balance">
|
||||
{guide.title}
|
||||
</h1>
|
||||
<p className="my-0 mb-6 flex items-center justify-start text-sm text-gray-400">
|
||||
<a
|
||||
href={`/authors/${guide.author?.slug}`}
|
||||
className="inline-flex items-center font-medium underline-offset-2 hover:text-gray-600 hover:underline"
|
||||
>
|
||||
<img
|
||||
alt={guide.author?.name}
|
||||
src={guide.author?.avatar}
|
||||
className="mr-2 mb-0 inline h-5 w-5 rounded-full"
|
||||
/>
|
||||
{guide.author?.name}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import type { OfficialGuideDocument } from '../../queries/official-guide';
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
type RelatedGuidesProps = {
|
||||
relatedTitle?: string;
|
||||
relatedGuides: Record<string, string>;
|
||||
relatedGuides: Pick<OfficialGuideDocument, 'title' | 'slug' | 'roadmapId'>[];
|
||||
};
|
||||
|
||||
export function RelatedGuides(props: RelatedGuidesProps) {
|
||||
@@ -12,14 +13,7 @@ export function RelatedGuides(props: RelatedGuidesProps) {
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const relatedGuidesArray = Object.entries(relatedGuides).map(
|
||||
([title, url]) => ({
|
||||
title,
|
||||
url,
|
||||
}),
|
||||
);
|
||||
|
||||
if (relatedGuidesArray.length === 0) {
|
||||
if (relatedGuides.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -47,23 +41,32 @@ export function RelatedGuides(props: RelatedGuidesProps) {
|
||||
isOpen && 'block',
|
||||
)}
|
||||
>
|
||||
{relatedGuidesArray.map((relatedGuide) => (
|
||||
<li key={relatedGuide.url}>
|
||||
<a
|
||||
href={relatedGuide.url}
|
||||
className="text-sm text-gray-500 no-underline hover:text-black max-lg:block max-lg:border-b max-lg:px-3 max-lg:py-1"
|
||||
onClick={() => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
{relatedGuides.map((relatedGuide) => {
|
||||
const { roadmapId, slug, title } = relatedGuide;
|
||||
const href = roadmapId ? `/${roadmapId}/${slug}` : `/guides/${slug}`;
|
||||
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
{relatedGuide.title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
const className = cn(
|
||||
'text-sm text-gray-500 no-underline hover:text-black max-lg:block max-lg:border-b max-lg:px-3 max-lg:py-1',
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={slug}>
|
||||
<a
|
||||
href={href}
|
||||
className={className}
|
||||
onClick={() => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Fragment, useEffect, useRef, useState } from 'react';
|
||||
import type { QuestionType } from '../../lib/question-group';
|
||||
import { markdownToHtml } from '../../lib/markdown';
|
||||
import Prism from 'prismjs';
|
||||
import './PrismAtom.css';
|
||||
import { guideRenderer, type QuestionType } from '../../lib/guide-renderer';
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
type QuestionCardProps = {
|
||||
question: QuestionType;
|
||||
@@ -20,8 +18,6 @@ export function QuestionCard(props: QuestionCardProps) {
|
||||
// width if the answer is visible and the question height is less than
|
||||
// the answer height
|
||||
if (isAnswerVisible) {
|
||||
Prism.highlightAll();
|
||||
|
||||
const answerHeight = answerRef.current?.clientHeight || 0;
|
||||
const questionHeight = questionRef.current?.clientHeight || 0;
|
||||
|
||||
@@ -69,7 +65,7 @@ export function QuestionCard(props: QuestionCardProps) {
|
||||
</div>
|
||||
|
||||
<div className="mx-auto flex max-w-[550px] flex-1 items-center justify-center py-3 sm:py-8">
|
||||
<p className="px-4 text-xl font-semibold leading-snug! text-black sm:text-3xl">
|
||||
<p className="px-4 text-xl leading-snug! font-semibold text-black sm:text-3xl">
|
||||
{question.question}
|
||||
</p>
|
||||
</div>
|
||||
@@ -88,27 +84,15 @@ export function QuestionCard(props: QuestionCardProps) {
|
||||
|
||||
<div
|
||||
ref={answerRef}
|
||||
className={`absolute left-0 right-0 flex flex-col items-center justify-center rounded-[7px] bg-neutral-100 py-4 text-sm leading-normal text-black transition-all duration-300 sm:py-8 sm:text-xl ${
|
||||
isAnswerVisible ? 'top-0 min-h-[248px] sm:min-h-[398px]' : 'top-full'
|
||||
}`}
|
||||
className={cn(
|
||||
'absolute right-0 left-0 flex flex-col items-center justify-center rounded-[7px] bg-neutral-100 py-4 text-sm leading-normal text-black transition-all duration-300 sm:py-8 sm:text-xl',
|
||||
isAnswerVisible ? 'top-0 min-h-[248px] sm:min-h-[398px]' : 'top-full',
|
||||
)}
|
||||
>
|
||||
{!question.isLongAnswer && (
|
||||
<div
|
||||
className={`mx-auto flex max-w-[600px] grow flex-col items-center justify-center py-0 px-5 text-center text-base [&>p]:leading-relaxed sm:text-xl`}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: markdownToHtml(question.answer, false),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="qa-answer prose prose-h5:font-semibold prose-h5:mb-2 prose-h5:text-black prose-sm prose-quoteless prose-h1:mb-2.5 prose-h1:mt-7 prose-h2:mb-3 prose-h2:mt-0 prose-h3:mb-[5px] prose-h3:mt-[10px] prose-p:mb-2 prose-p:mt-0 prose-blockquote:font-normal prose-blockquote:not-italic prose-blockquote:text-gray-700 prose-pre:mb-6! prose-pre:w-full prose-ul:my-2 prose-li:m-0 prose-li:mb-0.5 prose-li:[&>p]:mb-0 sm:prose-p:mb-4 mx-auto flex w-full max-w-[600px] grow flex-col items-start justify-center px-4 py-0 text-left text-sm sm:px-5 sm:text-lg">
|
||||
{guideRenderer.render(question.answer)}
|
||||
</div>
|
||||
|
||||
{question.isLongAnswer && (
|
||||
<div
|
||||
className={`qa-answer prose prose-h5:font-semibold prose-h5:mb-2 prose-h5:text-black prose-sm prose-quoteless mx-auto flex w-full max-w-[600px] grow flex-col items-start justify-center py-0 px-4 text-left text-sm prose-h1:mb-2.5 prose-h1:mt-7 prose-h2:mb-3 prose-h2:mt-0 prose-h3:mb-[5px] prose-h3:mt-[10px] prose-p:mb-2 prose-p:mt-0 prose-blockquote:font-normal prose-blockquote:not-italic prose-blockquote:text-gray-700 prose-pre:mb-6! prose-pre:w-full prose-ul:my-2 prose-li:m-0 prose-li:mb-0.5 sm:px-5 sm:text-lg sm:prose-p:mb-4`}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: markdownToHtml(question.answer, false),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="mt-7 text-center">
|
||||
<button
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
---
|
||||
import { getGuideTableOfContent, type HeadingGroupType } from '../../lib/guide';
|
||||
import { markdownToHtml } from '../../lib/markdown';
|
||||
import {
|
||||
type QuestionGroupType,
|
||||
type QuestionType,
|
||||
} from '../../lib/question-group';
|
||||
import { slugify } from '../../lib/slugger';
|
||||
import { RelatedGuides } from '../Guide/RelatedGuides';
|
||||
import MarkdownFile from '../MarkdownFile.astro';
|
||||
import { TableOfContent } from '../TableOfContent/TableOfContent';
|
||||
import { QuestionsList } from './QuestionsList';
|
||||
|
||||
interface Props {
|
||||
questionGroup: QuestionGroupType;
|
||||
}
|
||||
|
||||
const { questionGroup } = Astro.props;
|
||||
|
||||
const { frontmatter: guideFrontmatter, author } = questionGroup;
|
||||
|
||||
// Group questions by topics
|
||||
const questionsGroupedByTopics = questionGroup.questions.reduce(
|
||||
(acc, question) => {
|
||||
question.topics?.forEach((topic) => {
|
||||
acc[topic] = [...(acc[topic] || []), question];
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, QuestionType[]>,
|
||||
);
|
||||
|
||||
// Get all unique topics in the order they appear in the questions array
|
||||
const topicsInOrder: string[] = [];
|
||||
questionGroup.questions.forEach((question) => {
|
||||
question.topics?.forEach((topic) => {
|
||||
if (!topicsInOrder.includes(topic)) {
|
||||
topicsInOrder.push(topic);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const allHeadings = questionGroup.getHeadings();
|
||||
let tableOfContent: HeadingGroupType[] = [
|
||||
...getGuideTableOfContent(allHeadings),
|
||||
{
|
||||
depth: 2,
|
||||
children: [],
|
||||
slug: 'test-with-flashcards',
|
||||
text: 'Test yourself with Flashcards',
|
||||
},
|
||||
{
|
||||
depth: 2,
|
||||
children: topicsInOrder.map((topic) => {
|
||||
let topicText = topic;
|
||||
let topicSlug = slugify(topic);
|
||||
if (topic.toLowerCase() === 'beginners') {
|
||||
topicText = 'Beginner Level';
|
||||
topicSlug = 'beginner-level';
|
||||
} else if (topic.toLowerCase() === 'intermediate') {
|
||||
topicText = 'Intermediate Level';
|
||||
topicSlug = 'intermediate-level';
|
||||
} else if (topic.toLowerCase() === 'advanced') {
|
||||
topicText = 'Advanced Level';
|
||||
topicSlug = 'advanced-level';
|
||||
}
|
||||
|
||||
return {
|
||||
depth: 2,
|
||||
children: [],
|
||||
slug: topicSlug,
|
||||
text: topicText,
|
||||
};
|
||||
}),
|
||||
slug: 'questions-list',
|
||||
text: 'Questions List',
|
||||
},
|
||||
];
|
||||
|
||||
const showTableOfContent = tableOfContent.length > 0;
|
||||
---
|
||||
|
||||
<article class='lg:grid lg:max-w-full lg:grid-cols-[1fr_minmax(0,700px)_1fr]'>
|
||||
{
|
||||
showTableOfContent && (
|
||||
<div class='bg-linear-to-r from-gray-50 py-0 lg:col-start-3 lg:col-end-4 lg:row-start-1'>
|
||||
<RelatedGuides
|
||||
relatedTitle={guideFrontmatter?.relatedTitle}
|
||||
relatedGuides={questionGroup?.relatedGuides || {}}
|
||||
client:load
|
||||
/>
|
||||
<TableOfContent toc={tableOfContent} client:load />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div
|
||||
class:list={[
|
||||
'col-start-2 col-end-3 row-start-1 mx-auto max-w-[700px] py-5 sm:py-10',
|
||||
{
|
||||
'lg:border-r': showTableOfContent,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<MarkdownFile>
|
||||
<h1 class='mb-3 text-4xl font-bold text-balance'>
|
||||
{guideFrontmatter.title}
|
||||
</h1>
|
||||
{
|
||||
author && (
|
||||
<p class='my-0 flex items-center justify-start text-sm text-gray-400'>
|
||||
<a
|
||||
href={`/authors/${author?.id}`}
|
||||
class='inline-flex items-center font-medium underline-offset-2 hover:text-gray-600 hover:underline'
|
||||
>
|
||||
<img
|
||||
alt={author.frontmatter.name}
|
||||
src={author.frontmatter.imageUrl}
|
||||
class='mr-2 mb-0 inline h-5 w-5 rounded-full'
|
||||
/>
|
||||
{author.frontmatter.name}
|
||||
</a>
|
||||
<span class='mx-2 hidden sm:inline'>·</span>
|
||||
<a
|
||||
class='hidden underline-offset-2 hover:text-gray-600 sm:inline'
|
||||
href={`https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/question-groups/${questionGroup.id}`}
|
||||
target='_blank'
|
||||
>
|
||||
Improve this Guide
|
||||
</a>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
<questionGroup.Content />
|
||||
|
||||
<h2 id='test-with-flashcards'>Test yourself with Flashcards</h2>
|
||||
<p>
|
||||
You can either use these flashcards or jump to the questions list
|
||||
section below to see them in a list format.
|
||||
</p>
|
||||
<div class='mx-0 sm:-mb-32'>
|
||||
<QuestionsList
|
||||
groupId={questionGroup.id}
|
||||
questions={questionGroup.questions}
|
||||
client:load
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h2 id='questions-list'>Questions List</h2>
|
||||
<p>
|
||||
If you prefer to see the questions in a list format, you can find them
|
||||
below.
|
||||
</p>
|
||||
|
||||
{
|
||||
topicsInOrder.map((questionLevel) => (
|
||||
<div class='mb-5'>
|
||||
<h3 id={slugify(questionLevel)} class='mb-0 capitalize'>
|
||||
{questionLevel.toLowerCase() === 'beginners' ? 'Beginner Level' :
|
||||
questionLevel.toLowerCase() === 'intermediate' ? 'Intermediate Level' :
|
||||
questionLevel.toLowerCase() === 'advanced' ? 'Advanced Level' :
|
||||
questionLevel}
|
||||
</h3>
|
||||
{questionsGroupedByTopics[questionLevel].map((q) => (
|
||||
<div class='mb-5'>
|
||||
<h4>{q.question}</h4>
|
||||
<div set:html={markdownToHtml(q.answer, false)} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
{
|
||||
questionGroup.ending && (
|
||||
<div class='mb-5'>
|
||||
<div set:html={markdownToHtml(questionGroup.ending, false)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</MarkdownFile>
|
||||
</div>
|
||||
</article>
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import type { QuestionType } from '../../lib/guide-renderer';
|
||||
import { QuestionsProgress } from './QuestionsProgress';
|
||||
import { CheckCircle, SkipForward, Sparkles } from 'lucide-react';
|
||||
import { QuestionCard } from './QuestionCard';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
import type { QuestionType } from '../../lib/question-group';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { QuestionFinished } from './QuestionFinished';
|
||||
import { QuestionCard } from './QuestionCard';
|
||||
import { CheckCircleIcon, SkipForwardIcon, SparklesIcon } from 'lucide-react';
|
||||
import { Confetti } from '../Confetti';
|
||||
|
||||
type UserQuestionProgress = {
|
||||
@@ -16,12 +16,12 @@ type UserQuestionProgress = {
|
||||
export type QuestionProgressType = keyof UserQuestionProgress;
|
||||
|
||||
type QuestionsListProps = {
|
||||
groupId: string;
|
||||
questions: QuestionType[];
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function QuestionsList(props: QuestionsListProps) {
|
||||
const { questions } = props;
|
||||
const { questions, className } = props;
|
||||
|
||||
const [showConfetti, setShowConfetti] = useState(false);
|
||||
const [currQuestionIndex, setCurrQuestionIndex] = useState(0);
|
||||
@@ -73,7 +73,7 @@ export function QuestionsList(props: QuestionsListProps) {
|
||||
const hasFinished = hasProgress && currQuestionIndex === -1;
|
||||
|
||||
return (
|
||||
<div className="mb-0 gap-3 text-center sm:mb-40">
|
||||
<div className={cn('mb-0 gap-3 text-center sm:mb-40', className)}>
|
||||
<QuestionsProgress
|
||||
knowCount={knowCount}
|
||||
didNotKnowCount={dontKnowCount}
|
||||
@@ -139,9 +139,10 @@ export function QuestionsList(props: QuestionsListProps) {
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`flex flex-col gap-1 transition-opacity duration-300 sm:flex-row sm:gap-3 ${
|
||||
hasFinished ? 'opacity-0' : 'opacity-100'
|
||||
}`}
|
||||
className={cn(
|
||||
'flex flex-col gap-1 transition-opacity duration-300 sm:flex-row sm:gap-3',
|
||||
hasFinished ? 'opacity-0' : 'opacity-100',
|
||||
)}
|
||||
>
|
||||
<button
|
||||
disabled={!currQuestion}
|
||||
@@ -152,7 +153,7 @@ export function QuestionsList(props: QuestionsListProps) {
|
||||
}}
|
||||
className="flex flex-1 items-center rounded-md border border-gray-300 bg-white px-2 py-2 text-sm text-black transition-colors hover:border-black hover:bg-black hover:text-white disabled:pointer-events-none disabled:opacity-50 sm:rounded-lg sm:px-4 sm:py-3 sm:text-base"
|
||||
>
|
||||
<CheckCircle className="mr-1 h-4 text-current" />
|
||||
<CheckCircleIcon className="mr-1 h-4 text-current" />
|
||||
Already Know that
|
||||
</button>
|
||||
<button
|
||||
@@ -162,7 +163,7 @@ export function QuestionsList(props: QuestionsListProps) {
|
||||
disabled={!currQuestion}
|
||||
className="flex flex-1 items-center rounded-md border border-gray-300 bg-white px-2 py-2 text-sm text-black transition-colors hover:border-black hover:bg-black hover:text-white disabled:pointer-events-none disabled:opacity-50 sm:rounded-lg sm:px-4 sm:py-3 sm:text-base"
|
||||
>
|
||||
<Sparkles className="mr-1 h-4 text-current" />
|
||||
<SparklesIcon className="mr-1 h-4 text-current" />
|
||||
Didn't Know that
|
||||
</button>
|
||||
<button
|
||||
@@ -173,7 +174,7 @@ export function QuestionsList(props: QuestionsListProps) {
|
||||
data-next-question="skip"
|
||||
className="flex flex-1 items-center rounded-md border border-red-600 px-2 py-2 text-sm text-red-600 hover:bg-red-600 hover:text-white disabled:pointer-events-none disabled:opacity-50 sm:rounded-lg sm:px-4 sm:py-3 sm:text-base"
|
||||
>
|
||||
<SkipForward className="mr-1 h-4" />
|
||||
<SkipForwardIcon className="mr-1 h-4" />
|
||||
Skip Question
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -156,7 +156,7 @@ const hasProjects = projectCount > 0;
|
||||
)}
|
||||
{hasAIChat && (
|
||||
<TabLink
|
||||
url={`/${roadmapId}/ai`}
|
||||
url={`/ai/roadmap-chat/${roadmapId}`}
|
||||
icon={Bot}
|
||||
text='AI Tutor'
|
||||
mobileText='AI'
|
||||
|
||||
@@ -378,6 +378,11 @@ const groups: GroupType[] = [
|
||||
{
|
||||
group: 'Machine Learning',
|
||||
roadmaps: [
|
||||
{
|
||||
title: 'Machine Learning',
|
||||
link: '/machine-learning',
|
||||
type: 'role',
|
||||
},
|
||||
{
|
||||
title: 'AI and Data Scientist',
|
||||
link: '/ai-data-scientist',
|
||||
@@ -403,6 +408,11 @@ const groups: GroupType[] = [
|
||||
link: '/data-analyst',
|
||||
type: 'role',
|
||||
},
|
||||
{
|
||||
title: 'Data Engineer',
|
||||
link: '/data-engineer',
|
||||
type: 'role',
|
||||
},
|
||||
{
|
||||
title: 'MLOps',
|
||||
link: '/mlops',
|
||||
@@ -597,7 +607,7 @@ export function RoadmapsPage() {
|
||||
{isFilterOpen && <X size={13} className="mr-1" />}
|
||||
Categories
|
||||
</button>
|
||||
<div className="container relative flex flex-col gap-4 sm:flex-row">
|
||||
<div className="relative container flex flex-col gap-4 sm:flex-row">
|
||||
<div
|
||||
className={cn(
|
||||
'hidden w-full flex-col from-gray-100 sm:w-[180px] sm:border-r sm:bg-linear-to-l sm:pt-6',
|
||||
@@ -635,10 +645,10 @@ export function RoadmapsPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex grow flex-col gap-6 pb-20 pt-2 sm:pt-8">
|
||||
<div className="flex grow flex-col gap-6 pt-2 pb-20 sm:pt-8">
|
||||
{visibleGroups.map((group) => (
|
||||
<div key={`${group.group}-${group.roadmaps.length}`}>
|
||||
<h2 className="mb-2 text-xs uppercase tracking-wide text-gray-400">
|
||||
<h2 className="mb-2 text-xs tracking-wide text-gray-400 uppercase">
|
||||
{group.group}
|
||||
</h2>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import type { HeadingGroupType } from '../../lib/guide';
|
||||
import type { HeadingGroupType } from '../../lib/guide-renderer';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
@@ -23,7 +23,7 @@ export function TableOfContent(props: TableOfContentProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative min-w-[250px] px-5 pt-0 max-lg:min-w-full max-lg:max-w-full max-lg:border-none max-lg:px-0 lg:pt-5',
|
||||
'relative min-w-[250px] px-5 pt-0 max-lg:max-w-full max-lg:min-w-full max-lg:border-none max-lg:px-0 lg:pt-5',
|
||||
{
|
||||
'top-0 lg:sticky!': totalRows <= 20,
|
||||
},
|
||||
@@ -68,7 +68,7 @@ export function TableOfContent(props: TableOfContentProps) {
|
||||
</a>
|
||||
|
||||
{heading.children.length > 0 && (
|
||||
<ol className="my-0 ml-4 mt-1 space-y-0 max-lg:ml-0 max-lg:mt-0 max-lg:list-none">
|
||||
<ol className="my-0 mt-1 ml-4 space-y-0 max-lg:mt-0 max-lg:ml-0 max-lg:list-none">
|
||||
{heading.children.map((children) => {
|
||||
return (
|
||||
<li key={children.slug}>
|
||||
|
||||
@@ -17,7 +17,7 @@ const links = [
|
||||
isHighlighted: true,
|
||||
},
|
||||
{
|
||||
link: '/ai?format=roadmap',
|
||||
link: '/ai/roadmap',
|
||||
label: 'AI Roadmaps',
|
||||
description: 'Generate roadmaps with AI',
|
||||
Icon: Sparkles,
|
||||
|
||||
@@ -25,9 +25,7 @@ import { Ban, FileText, HeartHandshake, Star, X } from 'lucide-react';
|
||||
import { getUrlParams, parseUrl } from '../../lib/browser';
|
||||
import { Spinner } from '../ReactIcons/Spinner';
|
||||
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
|
||||
import {
|
||||
type AllowedRoadmapRenderer
|
||||
} from '../../lib/roadmap.ts';
|
||||
import { type AllowedRoadmapRenderer } from '../../lib/roadmap.ts';
|
||||
import { lockBodyScroll } from '../../lib/dom.ts';
|
||||
import { TopicDetailLink } from './TopicDetailLink.tsx';
|
||||
import { ResourceListSeparator } from './ResourceListSeparator.tsx';
|
||||
@@ -42,6 +40,8 @@ import type { AIChatHistoryType } from '../GenerateCourse/AICourseLessonChat.tsx
|
||||
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal.tsx';
|
||||
import { TopicProgressButton } from './TopicProgressButton.tsx';
|
||||
import { CreateCourseModal } from './CreateCourseModal.tsx';
|
||||
import { useChat } from '@ai-sdk/react';
|
||||
import { topicDetailAiChatTransport } from '../../lib/ai.ts';
|
||||
|
||||
type PaidResourceType = {
|
||||
_id?: string;
|
||||
@@ -134,8 +134,6 @@ export function TopicDetail(props: TopicDetailProps) {
|
||||
const [links, setLinks] = useState<RoadmapContentDocument['links']>([]);
|
||||
const [activeTab, setActiveTab] =
|
||||
useState<AllowedTopicDetailsTabs>(defaultActiveTab);
|
||||
const [aiChatHistory, setAiChatHistory] =
|
||||
useState<AIChatHistoryType[]>(defaultChatHistory);
|
||||
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
|
||||
const [isCustomResource, setIsCustomResource] = useState(false);
|
||||
|
||||
@@ -156,14 +154,20 @@ export function TopicDetail(props: TopicDetailProps) {
|
||||
const [resourceType, setResourceType] = useState<ResourceType>('roadmap');
|
||||
const [paidResources, setPaidResources] = useState<PaidResourceType[]>([]);
|
||||
|
||||
const chatId = `${resourceType}-${resourceId}-${topicId}`;
|
||||
const { messages, sendMessage, setMessages, status } = useChat({
|
||||
id: chatId,
|
||||
transport: topicDetailAiChatTransport,
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
onClose?.();
|
||||
setIsActive(false);
|
||||
setIsContributing(false);
|
||||
setShowUpgradeModal(false);
|
||||
setAiChatHistory(defaultChatHistory);
|
||||
setActiveTab('content');
|
||||
setShowSubjectSearchModal(false);
|
||||
setMessages([]);
|
||||
|
||||
lockBodyScroll(false);
|
||||
|
||||
@@ -485,8 +489,10 @@ export function TopicDetail(props: TopicDetailProps) {
|
||||
resourceId={resourceId}
|
||||
resourceType={resourceType}
|
||||
topicId={topicId}
|
||||
aiChatHistory={aiChatHistory}
|
||||
setAiChatHistory={setAiChatHistory}
|
||||
messages={messages}
|
||||
setMessages={setMessages}
|
||||
status={status}
|
||||
sendMessage={sendMessage}
|
||||
hasUpgradeButtons={hasUpgradeButtons}
|
||||
onUpgrade={() => setShowUpgradeModal(true)}
|
||||
onLogin={() => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import '../ChatMessages/AIChat.css';
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
BotIcon,
|
||||
@@ -9,14 +11,11 @@ import {
|
||||
Trash2,
|
||||
WandSparkles,
|
||||
} from 'lucide-react';
|
||||
import { Fragment, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { readStream } from '../../lib/ai';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { isLoggedIn, removeAuthToken } from '../../lib/jwt';
|
||||
import { markdownToHtmlWithHighlighting } from '../../lib/markdown';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
import { getPercentage } from '../../lib/number';
|
||||
import { showLoginPopup } from '../../lib/popup';
|
||||
import type { ResourceType } from '../../lib/resource-progress';
|
||||
@@ -24,14 +23,12 @@ import { aiLimitOptions } from '../../queries/ai-course';
|
||||
import { billingDetailsOptions } from '../../queries/billing';
|
||||
import { roadmapTreeMappingOptions } from '../../queries/roadmap-tree';
|
||||
import { queryClient } from '../../stores/query-client';
|
||||
import {
|
||||
AIChatCard,
|
||||
type AIChatHistoryType,
|
||||
} from '../GenerateCourse/AICourseLessonChat';
|
||||
import '../GenerateCourse/AICourseLessonChat.css';
|
||||
import { AILimitsPopup } from '../GenerateCourse/AILimitsPopup';
|
||||
import { PredefinedActions, promptLabelMapping } from './PredefinedActions';
|
||||
import { defaultChatHistory } from './TopicDetail';
|
||||
import { PredefinedActions } from './PredefinedActions';
|
||||
import type { ChatStatus, UIMessage } from 'ai';
|
||||
import type { UseChatHelpers } from '@ai-sdk/react';
|
||||
import { useAIChatScroll } from '../../hooks/use-ai-chat-scroll';
|
||||
import { TopicChatMessages } from '../ChatMessages/TopicChatMessages';
|
||||
|
||||
type TopicDetailAIProps = {
|
||||
resourceId: string;
|
||||
@@ -40,8 +37,10 @@ type TopicDetailAIProps = {
|
||||
|
||||
hasUpgradeButtons?: boolean;
|
||||
|
||||
aiChatHistory: AIChatHistoryType[];
|
||||
setAiChatHistory: (history: AIChatHistoryType[]) => void;
|
||||
messages: UIMessage[];
|
||||
sendMessage: UseChatHelpers<UIMessage>['sendMessage'];
|
||||
setMessages: UseChatHelpers<UIMessage>['setMessages'];
|
||||
status: ChatStatus;
|
||||
|
||||
onUpgrade: () => void;
|
||||
onLogin: () => void;
|
||||
@@ -51,8 +50,11 @@ type TopicDetailAIProps = {
|
||||
|
||||
export function TopicDetailAI(props: TopicDetailAIProps) {
|
||||
const {
|
||||
aiChatHistory,
|
||||
setAiChatHistory,
|
||||
messages,
|
||||
sendMessage,
|
||||
setMessages,
|
||||
status,
|
||||
|
||||
resourceId,
|
||||
resourceType,
|
||||
topicId,
|
||||
@@ -63,7 +65,6 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
||||
} = props;
|
||||
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const scrollareaRef = useRef<HTMLDivElement>(null);
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
const sanitizedTopicId = topicId?.includes('@')
|
||||
@@ -72,8 +73,6 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
||||
|
||||
const toast = useToast();
|
||||
const [message, setMessage] = useState('');
|
||||
const [isStreamingMessage, setIsStreamingMessage] = useState(false);
|
||||
const [streamedMessage, setStreamedMessage] = useState('');
|
||||
const [showAILimitsPopup, setShowAILimitsPopup] = useState(false);
|
||||
const { data: tokenUsage, isLoading } = useQuery(
|
||||
aiLimitOptions(),
|
||||
@@ -105,7 +104,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
||||
|
||||
if (
|
||||
!trimmedMessage ||
|
||||
isStreamingMessage ||
|
||||
status !== 'ready' ||
|
||||
!isLoggedIn() ||
|
||||
isLimitExceeded ||
|
||||
isLoading
|
||||
@@ -113,110 +112,30 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newMessages: AIChatHistoryType[] = [
|
||||
...aiChatHistory,
|
||||
sendMessage(
|
||||
{
|
||||
role: 'user',
|
||||
content: trimmedMessage,
|
||||
text: trimmedMessage,
|
||||
},
|
||||
];
|
||||
{
|
||||
body: {
|
||||
resourceId,
|
||||
resourceType,
|
||||
topicId: sanitizedTopicId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
flushSync(() => {
|
||||
setAiChatHistory(newMessages);
|
||||
setMessage('');
|
||||
});
|
||||
|
||||
scrollToBottom();
|
||||
completeAITutorChat(newMessages);
|
||||
setMessage('');
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
textareaRef.current?.focus();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const scrollToBottom = useCallback(() => {
|
||||
scrollareaRef.current?.scrollTo({
|
||||
top: scrollareaRef.current.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
const { scrollToBottom, scrollableContainerRef, showScrollToBottomButton } =
|
||||
useAIChatScroll({
|
||||
messages,
|
||||
});
|
||||
}, [scrollareaRef]);
|
||||
|
||||
const completeAITutorChat = async (messages: AIChatHistoryType[]) => {
|
||||
try {
|
||||
setIsStreamingMessage(true);
|
||||
|
||||
const response = await fetch(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-topic-detail-chat`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
resourceId,
|
||||
resourceType,
|
||||
topicId: sanitizedTopicId,
|
||||
messages: messages.slice(-10),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
|
||||
toast.error(data?.message || 'Something went wrong');
|
||||
setAiChatHistory([...messages].slice(0, messages.length - 1));
|
||||
setIsStreamingMessage(false);
|
||||
|
||||
if (data.status === 401) {
|
||||
removeAuthToken();
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
queryClient.invalidateQueries(aiLimitOptions());
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
|
||||
if (!reader) {
|
||||
setIsStreamingMessage(false);
|
||||
toast.error('Something went wrong');
|
||||
return;
|
||||
}
|
||||
|
||||
await readStream(reader, {
|
||||
onStream: async (content) => {
|
||||
flushSync(() => {
|
||||
setStreamedMessage(content);
|
||||
});
|
||||
|
||||
scrollToBottom();
|
||||
},
|
||||
onStreamEnd: async (content) => {
|
||||
const newMessages: AIChatHistoryType[] = [
|
||||
...messages,
|
||||
{
|
||||
role: 'assistant',
|
||||
content,
|
||||
html: await markdownToHtmlWithHighlighting(content),
|
||||
},
|
||||
];
|
||||
|
||||
flushSync(() => {
|
||||
setStreamedMessage('');
|
||||
setIsStreamingMessage(false);
|
||||
setAiChatHistory(newMessages);
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries(aiLimitOptions());
|
||||
scrollToBottom();
|
||||
},
|
||||
});
|
||||
|
||||
setIsStreamingMessage(false);
|
||||
} catch (error) {
|
||||
toast.error('Something went wrong');
|
||||
setIsStreamingMessage(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
@@ -228,7 +147,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
||||
tokenUsage?.used || 0,
|
||||
tokenUsage?.limit || 0,
|
||||
);
|
||||
const hasChatHistory = aiChatHistory.length > 1;
|
||||
const hasChatHistory = messages.length > 0;
|
||||
const nodeTextParts = roadmapTreeMapping?.text?.split('>') || [];
|
||||
const hasSubjects =
|
||||
(roadmapTreeMapping?.subjects &&
|
||||
@@ -236,7 +155,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
||||
nodeTextParts.length > 1;
|
||||
|
||||
return (
|
||||
<div className="relative mt-4 flex grow flex-col overflow-hidden rounded-lg border border-gray-200">
|
||||
<div className="ai-chat relative mt-4 flex grow flex-col overflow-hidden rounded-lg border border-gray-200">
|
||||
{isDataLoading && (
|
||||
<div className="absolute inset-0 z-20 flex items-center justify-center gap-2 bg-white text-black">
|
||||
<Loader2Icon className="size-8 animate-spin stroke-3 text-gray-500" />
|
||||
@@ -278,8 +197,8 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
||||
return;
|
||||
}
|
||||
}}
|
||||
href={`/ai/course?term=${subject}&difficulty=beginner&src=topic`}
|
||||
className="flex items-center gap-1 gap-2 rounded-md border border-gray-300 bg-gray-100 px-2 py-1 hover:bg-gray-200 hover:text-black"
|
||||
href={`/ai/course/search?term=${subject}&difficulty=beginner&src=topic`}
|
||||
className="flex items-center gap-1 rounded-md border border-gray-300 bg-gray-100 px-2 py-1 hover:bg-gray-200 hover:text-black"
|
||||
>
|
||||
{subject}
|
||||
</a>
|
||||
@@ -289,7 +208,20 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
||||
{roadmapTreeMapping?.subjects?.length === 0 && (
|
||||
<a
|
||||
target="_blank"
|
||||
href={`/ai/course?term=${roadmapTreeMapping?.text}&difficulty=beginner&src=topic`}
|
||||
onClick={(e) => {
|
||||
if (!isLoggedIn()) {
|
||||
e.preventDefault();
|
||||
onLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLimitExceeded) {
|
||||
e.preventDefault();
|
||||
onUpgrade();
|
||||
return;
|
||||
}
|
||||
}}
|
||||
href={`/ai/course/search?term=${roadmapTreeMapping?.text}&difficulty=beginner&src=topic`}
|
||||
className="flex items-center gap-1 rounded-md border border-gray-300 bg-gray-100 px-2 py-1 hover:bg-gray-200 hover:text-black [&>svg:last-child]:hidden"
|
||||
>
|
||||
{nodeTextParts.slice(-2).map((text, index) => {
|
||||
@@ -349,7 +281,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
||||
<button
|
||||
className="rounded-md bg-white px-2 py-2 text-xs font-medium text-black hover:bg-gray-200"
|
||||
onClick={() => {
|
||||
setAiChatHistory(defaultChatHistory);
|
||||
setMessages([]);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
@@ -416,39 +348,9 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
||||
|
||||
<div
|
||||
className="scrollbar-thumb-gray-300 scrollbar-track-transparent scrollbar-thin relative grow overflow-y-auto"
|
||||
ref={scrollareaRef}
|
||||
ref={scrollableContainerRef}
|
||||
>
|
||||
<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">
|
||||
{aiChatHistory.map((chat, index) => {
|
||||
let content = chat.content;
|
||||
|
||||
if (chat.role === 'user' && promptLabelMapping[chat.content]) {
|
||||
content = promptLabelMapping[chat.content];
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment key={`chat-${index}`}>
|
||||
<AIChatCard
|
||||
role={chat.role}
|
||||
content={content}
|
||||
html={chat.html}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
{isStreamingMessage && !streamedMessage && (
|
||||
<AIChatCard role="assistant" content="Thinking..." />
|
||||
)}
|
||||
|
||||
{streamedMessage && (
|
||||
<AIChatCard role="assistant" content={streamedMessage} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TopicChatMessages messages={messages} status={status} />
|
||||
</div>
|
||||
|
||||
<form
|
||||
@@ -517,7 +419,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isStreamingMessage || isLimitExceeded}
|
||||
disabled={status !== 'ready' || isLimitExceeded}
|
||||
className="flex aspect-square size-[41px] items-center justify-center text-zinc-500 hover:text-black disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<SendIcon className="size-4 stroke-[2.5]" />
|
||||
|
||||
@@ -14,4 +14,5 @@ function App() {
|
||||
<LazyComponent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,4 +1,4 @@
|
||||
Raw string literals in Go are enclosed in backticks (`` ` ``) and preserve all formatting exactly as written. This is different from interpreted string literals, which process escape sequences like \n. This distinction is particularly useful when you need to process data exactly as it is written.
|
||||
Raw string literals in Go are enclosed in backticks (\`) and preserve all formatting exactly as written. This is different from interpreted string literals, which process escape sequences like \n. This distinction is particularly useful when you need to process data exactly as it is written.
|
||||
|
||||
Consider a scenario where you need to embed an HTML template directly into your Go code. With raw string literals, you can include the HTML exactly as written without worrying about escaping characters or preserving the formatting. For example:
|
||||
|
||||
@@ -11,7 +11,7 @@ htmlTemplate := `<!DOCTYPE html>
|
||||
<body>
|
||||
<h1>Hello, World!</h1>
|
||||
</body>
|
||||
</html>`
|
||||
</html>` // backtick here ends the raw string literal
|
||||
```
|
||||
|
||||
In this case, the raw string literal enclosed in backticks preserves newlines, tabs, and any other whitespace exactly as you write them.
|
||||
@@ -19,4 +19,5 @@ const styleP = document.getElementById("styleP");
|
||||
styleP.style.color = "red";
|
||||
styleP.style.border = "3px solid black";
|
||||
|
||||
console.log(styleP.style);
|
||||
console.log(styleP.style);
|
||||
```
|
||||
@@ -2,21 +2,14 @@ Function scope refers to the scope of variables defined within a function. You c
|
||||
|
||||
```javascript
|
||||
function myStudyPlan() {
|
||||
var studyPlanOne = "Top JavaScript interview questions for web developers";
|
||||
let studyPlanTwo = "Top JavaScript interview questions for web developers";
|
||||
const studyPlanThree = "Top JavaScript interview questions for web developers";
|
||||
|
||||
var studyPlanOne = "Top JavaScript interview questions for web developers";
|
||||
|
||||
let studyPlanTwo = "Top JavaScript interview questions for web developers";
|
||||
|
||||
const studyPlanThree = "Top JavaScript interview questions for web developers";
|
||||
|
||||
|
||||
|
||||
console.log(studyPlanOne);
|
||||
|
||||
console.log(studyPlanTwo);
|
||||
|
||||
console.log(studyPlanThree);
|
||||
|
||||
console.log(studyPlanOne);
|
||||
console.log(studyPlanTwo);
|
||||
console.log(studyPlanThree);
|
||||
}
|
||||
|
||||
myStudyPlan(); // Calls the function
|
||||
myStudyPlan(); // Calls the function
|
||||
```
|
||||
@@ -13,4 +13,5 @@ Immediately invoked function expressions, or IIFEs, run as soon as they're creat
|
||||
console.log(
|
||||
"roadmap.sh helps prepare for JavaScript job interview questions"
|
||||
);
|
||||
})();
|
||||
})();
|
||||
```
|
||||
@@ -24,4 +24,5 @@ export function studyJs(course) {
|
||||
|
||||
import { studyJs } from './app.js';
|
||||
|
||||
console.log(studyJs("roadmap.sh")); // Read the JavaScript guide on, roadmap.sh
|
||||
console.log(studyJs("roadmap.sh")); // Read the JavaScript guide on, roadmap.sh
|
||||
```
|
||||
@@ -30,4 +30,5 @@ console.log(courseNumber ); // code won't run
|
||||
|
||||
}
|
||||
|
||||
strictExample(); // ReferenceError
|
||||
strictExample(); // ReferenceError
|
||||
```
|
||||
32
src/data/question-groups/sql/content/where-vs-having.md
Normal file
32
src/data/question-groups/sql/content/where-vs-having.md
Normal file
@@ -0,0 +1,32 @@
|
||||
You use **WHERE** for filtering rows before applying any grouping or aggregation.
|
||||
The code snippet below illustrates the use of **WHERE**. It filters the `Users` table for rows where the `Age` is greater than 18.
|
||||
|
||||
```sql
|
||||
SELECT * FROM Users
|
||||
WHERE Age > 18;
|
||||
```
|
||||
|
||||
The result of the query is similar to the table below.
|
||||
|
||||
| userId | firstName | lastName | age |
|
||||
| ------ | --------- | -------- | --- |
|
||||
| 1 | John | Doe | 30 |
|
||||
| 2 | Jane | Don | 31 |
|
||||
| 3 | Will | Liam | 25 |
|
||||
| 4 | Wade | Great | 32 |
|
||||
| 5 | Peter | Smith | 27 |
|
||||
|
||||
On the other hand, you use **HAVING** to filter groups after performing grouping and aggregation. You apply it to the result of aggregate functions, and it is mostly used with the **GROUP BY** clause.
|
||||
|
||||
```sql
|
||||
SELECT FirstName, Age FROM Users
|
||||
GROUP BY FirstName, Age
|
||||
HAVING Age > 30;
|
||||
```
|
||||
|
||||
The code above selects the `FirstName` and `Age` columns, then groups by the `FirstName` and `Age`, and finally gets entries with age greater than 30. The result of the query looks like this:
|
||||
|
||||
| firstName | age |
|
||||
| --------- | --- |
|
||||
| Wade | 32 |
|
||||
| Jane | 31 |
|
||||
@@ -5,5 +5,5 @@ Code-generation agents take a plain language request, understand the goal, and t
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Multi-Agent-based Code Generation](https://arxiv.org/abs/2312.13010)
|
||||
- [@article@From Prompt to Production: Github Blog](https://github.blog/ai-and-ml/github-copilot/from-prompt-to-production-building-a-landing-page-with-copilot-agent-mode/)
|
||||
- [@official@Github Copilot](https://github.com/features/copilot)
|
||||
- [@article@From Prompt to Production: GitHub Blog](https://github.blog/ai-and-ml/github-copilot/from-prompt-to-production-building-a-landing-page-with-copilot-agent-mode/)
|
||||
- [@official@GitHub Copilot](https://github.com/features/copilot)
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
jsonUrl: '/jsons/roadmaps/ai-data-scientist.json'
|
||||
pdfUrl: '/pdfs/roadmaps/ai-data-scientist.pdf'
|
||||
order: 5
|
||||
order: 4.5
|
||||
renderer: 'editor'
|
||||
briefTitle: 'AI and Data Scientist'
|
||||
briefDescription: 'Step by step guide to becoming an AI and Data Scientist in 2025'
|
||||
|
||||
@@ -8,7 +8,7 @@ briefDescription: 'Step by step guide to becoming an AI Engineer in 2025'
|
||||
title: 'AI Engineer'
|
||||
description: 'Step by step guide to becoming an AI Engineer in 2025'
|
||||
hasTopics: true
|
||||
isNew: true
|
||||
isNew: false
|
||||
dimensions:
|
||||
width: 968
|
||||
height: 3200
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
pdfUrl: '/pdfs/roadmaps/android.pdf'
|
||||
renderer: 'editor'
|
||||
order: 5
|
||||
order: 4.7
|
||||
briefTitle: 'Android'
|
||||
briefDescription: 'Step by step guide to becoming an Android Developer in 2025'
|
||||
title: 'Android Developer'
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@roadmap@Git and Github Roadmap](https://roadmap.sh/git-github)
|
||||
- [@roadmap@Git and GitHub Roadmap](https://roadmap.sh/git-github)
|
||||
- [@official@Git](https://git-scm.com/)
|
||||
- [@official@Git Documentation](https://git-scm.com/docs)
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@roadmap@Git and Github Roadmap](https://roadmap.sh/git-github)
|
||||
- [@roadmap@Git and GitHub Roadmap](https://roadmap.sh/git-github)
|
||||
- [@official@GitHub](https://github.com/)
|
||||
- [@official@Github Documentation](https://docs.github.com/)
|
||||
- [@official@GitHub Documentation](https://docs.github.com/)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# GitLab
|
||||
|
||||
`Gitlab` is a web-based DevOps lifecycle tool which provides a Git-repository manager, along with continuous integration and deployment pipeline features, using an open-source license, developed by GitLab Inc. Users can manage and create their software projects and repositories, and collaborate on these projects with other members. `Gitlab` also allows users to view analytics and open issues of their project. It stands next to other version control tools like `GitHub` and `Bitbucket`, but comes with its own set of additional features and nuances. For Android development, `Gitlab` can be particularly useful owing to its continuous integration and deployment system which can automate large parts of the app testing and deployment.
|
||||
`GitLab` is a web-based DevOps lifecycle tool which provides a Git-repository manager, along with continuous integration and deployment pipeline features, using an open-source license, developed by GitLab Inc. Users can manage and create their software projects and repositories, and collaborate on these projects with other members. `GitLab` also allows users to view analytics and open issues of their project. It stands next to other version control tools like `GitHub` and `Bitbucket`, but comes with its own set of additional features and nuances. For Android development, `GitLab` can be particularly useful owing to its continuous integration and deployment system which can automate large parts of the app testing and deployment.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@official@Gitlab](https://about.gitlab.com/)
|
||||
- [@official@Gitlab Documentation](https://docs.gitlab.com/)
|
||||
- [@official@GitLab](https://about.gitlab.com/)
|
||||
- [@official@GitLab Documentation](https://docs.gitlab.com/)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Github Actions
|
||||
# GitHub Actions
|
||||
|
||||
GitHub Actions is a powerful and flexible automation platform that enables developers to create custom workflows for their software development lifecycle (SDLC) directly in their GitHub repository. It allows developers to automate various tasks, such as building, testing, and deploying code, directly from their GitHub repository.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Metrics
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
Learn more from the following resources:
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@ Visit the following resources to learn more:
|
||||
|
||||
- [@official@GitLab](https://gitlab.com/)
|
||||
- [@official@GitLab Documentation](https://docs.gitlab.com/)
|
||||
- [@video@What is Gitlab and Why Use It?](https://www.youtube.com/watch?v=bnF7f1zGpo4)
|
||||
- [@video@What is GitLab and Why Use It?](https://www.youtube.com/watch?v=bnF7f1zGpo4)
|
||||
- [@feed@Explore top posts about GitLab](https://app.daily.dev/tags/gitlab?ref=roadmapsh)
|
||||
|
||||
@@ -6,5 +6,5 @@ Visit the following resources to learn more:
|
||||
|
||||
- [@article@Solr Website](https://solr.apache.org/)
|
||||
- [@article@Solr Documentation](https://solr.apache.org/resources.html#documentation)
|
||||
- [@opensource@Solr on Github](https://github.com/apache/solr)
|
||||
- [@opensource@Solr on GitHub](https://github.com/apache/solr)
|
||||
- [@video@Apache Solr vs Elasticsearch Differences](https://www.youtube.com/watch?v=MMWBdSdbu5k)
|
||||
|
||||
@@ -4,7 +4,7 @@ GitHub is a provider of Internet hosting for software development and version co
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@roadmap@Visit Dedicated Github Roadmap](https://roadmap.sh/git-github)
|
||||
- [@roadmap@Visit Dedicated GitHub Roadmap](https://roadmap.sh/git-github)
|
||||
- [@official@GitHub](https://github.com)
|
||||
- [@official@GitHub Documentation](https://docs.github.com/en/get-started/quickstart)
|
||||
- [@video@What is GitHub?](https://www.youtube.com/watch?v=w3jLJU7DT5E)
|
||||
|
||||
@@ -6,7 +6,7 @@ briefTitle: 'Cloudflare'
|
||||
briefDescription: 'Learn to deploy your applications on Cloudflare'
|
||||
title: 'Cloudflare'
|
||||
description: 'Learn to deploy your applications on Cloudflare'
|
||||
isNew: true
|
||||
isNew: false
|
||||
hasTopics: true
|
||||
renderer: editor
|
||||
dimensions:
|
||||
|
||||
@@ -5,6 +5,6 @@ CI/CD (Continuous Integration/Continuous Deployment) pipelines automate the proc
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@official@Automate your workflow - Github Actions](https://github.com/features/actions)
|
||||
- [@official@CI/CD Pipelines - Gitlab](https://docs.gitlab.com/ee/ci/pipelines/)
|
||||
- [@official@CI/CD Pipelines - GitLab](https://docs.gitlab.com/ee/ci/pipelines/)
|
||||
- [@official@Continuous Integration and Delivery - CircleCI](https://circleci.com/)
|
||||
- [@official@Simple, Flexible, Trustworthy CI/CD Tools - Travis CI](https://www.travis-ci.com/)
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
Learn more from the following resources:
|
||||
|
||||
- [@official@memdump](https://www.kali.org/tools/memdump/)
|
||||
- [@opensource@memdump - Github](https://github.com/tchebb/memdump)
|
||||
- [@opensource@memdump - GitHub](https://github.com/tchebb/memdump)
|
||||
@@ -0,0 +1 @@
|
||||
# A/B Testing
|
||||
@@ -0,0 +1 @@
|
||||
# Amazon EC2 ( Compute)
|
||||
@@ -0,0 +1 @@
|
||||
# Amazon RDS (Database)
|
||||
@@ -0,0 +1 @@
|
||||
# Amazon RDS (Database)
|
||||
@@ -0,0 +1 @@
|
||||
# Amazon Redshift
|
||||
@@ -0,0 +1 @@
|
||||
# Apache Airflow
|
||||
@@ -0,0 +1 @@
|
||||
# Apache Hadoop YARN
|
||||
@@ -0,0 +1 @@
|
||||
# Apache Kafka
|
||||
@@ -0,0 +1 @@
|
||||
# Apache Spark
|
||||
@@ -0,0 +1 @@
|
||||
# APIs
|
||||
@@ -0,0 +1 @@
|
||||
# ArgoCD
|
||||
@@ -0,0 +1 @@
|
||||
# Async vs Sync Communication
|
||||
@@ -0,0 +1 @@
|
||||
# Aurora DB
|
||||
@@ -0,0 +1 @@
|
||||
# Authentication vs Authorization
|
||||
@@ -0,0 +1 @@
|
||||
# AWS CDK
|
||||
@@ -0,0 +1 @@
|
||||
# AWS EKS
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user