mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-19 23:53:24 +02:00
refactor: floating and topic ai
This commit is contained in:
committed by
Kamran Ahmed
parent
20c1a54198
commit
07b85c032a
10
package.json
10
package.json
@@ -32,6 +32,7 @@
|
|||||||
"test:e2e": "playwright test"
|
"test:e2e": "playwright test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ai-sdk/react": "2.0.0-beta.34",
|
||||||
"@astrojs/node": "^9.2.1",
|
"@astrojs/node": "^9.2.1",
|
||||||
"@astrojs/react": "^4.2.7",
|
"@astrojs/react": "^4.2.7",
|
||||||
"@astrojs/sitemap": "^3.4.0",
|
"@astrojs/sitemap": "^3.4.0",
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
"@radix-ui/react-popover": "^1.1.14",
|
"@radix-ui/react-popover": "^1.1.14",
|
||||||
"@resvg/resvg-js": "^2.6.2",
|
"@resvg/resvg-js": "^2.6.2",
|
||||||
"@roadmapsh/editor": "workspace:*",
|
"@roadmapsh/editor": "workspace:*",
|
||||||
|
"@shikijs/transformers": "^3.9.2",
|
||||||
"@tailwindcss/vite": "^4.1.7",
|
"@tailwindcss/vite": "^4.1.7",
|
||||||
"@tanstack/react-query": "^5.76.1",
|
"@tanstack/react-query": "^5.76.1",
|
||||||
"@tiptap/core": "^2.12.0",
|
"@tiptap/core": "^2.12.0",
|
||||||
@@ -65,6 +67,7 @@
|
|||||||
"image-size": "^2.0.2",
|
"image-size": "^2.0.2",
|
||||||
"jose": "^6.0.11",
|
"jose": "^6.0.11",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
|
"katex": "^0.16.22",
|
||||||
"lucide-react": "^0.511.0",
|
"lucide-react": "^0.511.0",
|
||||||
"luxon": "^3.6.1",
|
"luxon": "^3.6.1",
|
||||||
"markdown-it-async": "^2.2.0",
|
"markdown-it-async": "^2.2.0",
|
||||||
@@ -80,10 +83,14 @@
|
|||||||
"react-confetti": "^6.4.0",
|
"react-confetti": "^6.4.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
"react-resizable-panels": "^3.0.2",
|
"react-resizable-panels": "^3.0.2",
|
||||||
"react-textarea-autosize": "^8.5.9",
|
"react-textarea-autosize": "^8.5.9",
|
||||||
"react-tooltip": "^5.28.1",
|
"react-tooltip": "^5.28.1",
|
||||||
"rehype-external-links": "^3.0.0",
|
"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",
|
"remark-parse": "^11.0.0",
|
||||||
"roadmap-renderer": "^1.0.7",
|
"roadmap-renderer": "^1.0.7",
|
||||||
"sanitize-html": "^2.17.0",
|
"sanitize-html": "^2.17.0",
|
||||||
@@ -98,6 +105,7 @@
|
|||||||
"tiptap-markdown": "^0.8.10",
|
"tiptap-markdown": "^0.8.10",
|
||||||
"turndown": "^7.2.0",
|
"turndown": "^7.2.0",
|
||||||
"unified": "^11.0.5",
|
"unified": "^11.0.5",
|
||||||
|
"zod": "^4.0.17",
|
||||||
"zustand": "^5.0.4"
|
"zustand": "^5.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -113,7 +121,7 @@
|
|||||||
"@types/react-slick": "^0.23.13",
|
"@types/react-slick": "^0.23.13",
|
||||||
"@types/sanitize-html": "^2.16.0",
|
"@types/sanitize-html": "^2.16.0",
|
||||||
"@types/turndown": "^5.0.5",
|
"@types/turndown": "^5.0.5",
|
||||||
"ai": "^4.3.16",
|
"ai": "5.0.0-beta.34",
|
||||||
"csv-parser": "^3.2.0",
|
"csv-parser": "^3.2.0",
|
||||||
"gh-pages": "^6.3.0",
|
"gh-pages": "^6.3.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
|
474
pnpm-lock.yaml
generated
474
pnpm-lock.yaml
generated
@@ -8,6 +8,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
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':
|
'@astrojs/node':
|
||||||
specifier: ^9.2.1
|
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))
|
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':
|
'@roadmapsh/editor':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:packages/editor
|
version: link:packages/editor
|
||||||
|
'@shikijs/transformers':
|
||||||
|
specifier: ^3.9.2
|
||||||
|
version: 3.9.2
|
||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.1.7
|
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))
|
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:
|
js-cookie:
|
||||||
specifier: ^3.0.5
|
specifier: ^3.0.5
|
||||||
version: 3.0.5
|
version: 3.0.5
|
||||||
|
katex:
|
||||||
|
specifier: ^0.16.22
|
||||||
|
version: 0.16.22
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.511.0
|
specifier: ^0.511.0
|
||||||
version: 0.511.0(react@19.1.0)
|
version: 0.511.0(react@19.1.0)
|
||||||
@@ -152,6 +161,9 @@ importers:
|
|||||||
react-dropzone:
|
react-dropzone:
|
||||||
specifier: ^14.3.8
|
specifier: ^14.3.8
|
||||||
version: 14.3.8(react@19.1.0)
|
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:
|
react-resizable-panels:
|
||||||
specifier: ^3.0.2
|
specifier: ^3.0.2
|
||||||
version: 3.0.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
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:
|
rehype-external-links:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 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:
|
remark-parse:
|
||||||
specifier: ^11.0.0
|
specifier: ^11.0.0
|
||||||
version: 11.0.0
|
version: 11.0.0
|
||||||
@@ -206,13 +227,16 @@ importers:
|
|||||||
unified:
|
unified:
|
||||||
specifier: ^11.0.5
|
specifier: ^11.0.5
|
||||||
version: 11.0.5
|
version: 11.0.5
|
||||||
|
zod:
|
||||||
|
specifier: ^4.0.17
|
||||||
|
version: 4.0.17
|
||||||
zustand:
|
zustand:
|
||||||
specifier: ^5.0.4
|
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))
|
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:
|
devDependencies:
|
||||||
'@ai-sdk/google':
|
'@ai-sdk/google':
|
||||||
specifier: ^1.2.18
|
specifier: ^1.2.18
|
||||||
version: 1.2.18(zod@3.24.4)
|
version: 1.2.18(zod@4.0.17)
|
||||||
'@playwright/test':
|
'@playwright/test':
|
||||||
specifier: ^1.52.0
|
specifier: ^1.52.0
|
||||||
version: 1.52.0
|
version: 1.52.0
|
||||||
@@ -247,8 +271,8 @@ importers:
|
|||||||
specifier: ^5.0.5
|
specifier: ^5.0.5
|
||||||
version: 5.0.5
|
version: 5.0.5
|
||||||
ai:
|
ai:
|
||||||
specifier: ^4.3.16
|
specifier: 5.0.0-beta.34
|
||||||
version: 4.3.16(react@19.1.0)(zod@3.24.4)
|
version: 5.0.0-beta.34(zod@4.0.17)
|
||||||
csv-parser:
|
csv-parser:
|
||||||
specifier: ^3.2.0
|
specifier: ^3.2.0
|
||||||
version: 3.2.0
|
version: 3.2.0
|
||||||
@@ -263,7 +287,7 @@ importers:
|
|||||||
version: 14.1.0
|
version: 14.1.0
|
||||||
openai:
|
openai:
|
||||||
specifier: ^4.100.0
|
specifier: ^4.100.0
|
||||||
version: 4.100.0(zod@3.24.4)
|
version: 4.100.0(zod@4.0.17)
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.5.3
|
specifier: ^3.5.3
|
||||||
version: 3.5.3
|
version: 3.5.3
|
||||||
@@ -334,6 +358,12 @@ importers:
|
|||||||
|
|
||||||
packages:
|
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':
|
'@ai-sdk/google@1.2.18':
|
||||||
resolution: {integrity: sha512-8B70+i+uB12Ae6Sn6B9Oc6W0W/XorGgc88Nx0pyUrcxFOdytHBaAVhTPqYsO3LLClfjYN8pQ9GMxd5cpGEnUcA==}
|
resolution: {integrity: sha512-8B70+i+uB12Ae6Sn6B9Oc6W0W/XorGgc88Nx0pyUrcxFOdytHBaAVhTPqYsO3LLClfjYN8pQ9GMxd5cpGEnUcA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -346,26 +376,30 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.23.8
|
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':
|
'@ai-sdk/provider@1.1.3':
|
||||||
resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==}
|
resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@ai-sdk/react@1.2.12':
|
'@ai-sdk/provider@2.0.0-beta.2':
|
||||||
resolution: {integrity: sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==}
|
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'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18 || ^19 || ^19.0.0-rc
|
react: ^18 || ^19 || ^19.0.0-rc
|
||||||
zod: ^3.23.8
|
zod: ^3.25.76 || ^4
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
zod:
|
zod:
|
||||||
optional: true
|
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':
|
'@alloc/quick-lru@5.2.0':
|
||||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -1915,6 +1949,9 @@ packages:
|
|||||||
'@shikijs/core@3.4.2':
|
'@shikijs/core@3.4.2':
|
||||||
resolution: {integrity: sha512-AG8vnSi1W2pbgR2B911EfGqtLE9c4hQBYkv/x7Z+Kt0VxhgQKcW7UNDVYsu9YxwV6u+OJrvdJrMq6DNWoBjihQ==}
|
resolution: {integrity: sha512-AG8vnSi1W2pbgR2B911EfGqtLE9c4hQBYkv/x7Z+Kt0VxhgQKcW7UNDVYsu9YxwV6u+OJrvdJrMq6DNWoBjihQ==}
|
||||||
|
|
||||||
|
'@shikijs/core@3.9.2':
|
||||||
|
resolution: {integrity: sha512-3q/mzmw09B2B6PgFNeiaN8pkNOixWS726IHmJEpjDAcneDPMQmUg2cweT9cWXY4XcyQS3i6mOOUgQz9RRUP6HA==}
|
||||||
|
|
||||||
'@shikijs/engine-javascript@3.4.2':
|
'@shikijs/engine-javascript@3.4.2':
|
||||||
resolution: {integrity: sha512-1/adJbSMBOkpScCE/SB6XkjJU17ANln3Wky7lOmrnpl+zBdQ1qXUJg2GXTYVHRq+2j3hd1DesmElTXYDgtfSOQ==}
|
resolution: {integrity: sha512-1/adJbSMBOkpScCE/SB6XkjJU17ANln3Wky7lOmrnpl+zBdQ1qXUJg2GXTYVHRq+2j3hd1DesmElTXYDgtfSOQ==}
|
||||||
|
|
||||||
@@ -1927,9 +1964,15 @@ packages:
|
|||||||
'@shikijs/themes@3.4.2':
|
'@shikijs/themes@3.4.2':
|
||||||
resolution: {integrity: sha512-qAEuAQh+brd8Jyej2UDDf+b4V2g1Rm8aBIdvt32XhDPrHvDkEnpb7Kzc9hSuHUxz0Iuflmq7elaDuQAP9bHIhg==}
|
resolution: {integrity: sha512-qAEuAQh+brd8Jyej2UDDf+b4V2g1Rm8aBIdvt32XhDPrHvDkEnpb7Kzc9hSuHUxz0Iuflmq7elaDuQAP9bHIhg==}
|
||||||
|
|
||||||
|
'@shikijs/transformers@3.9.2':
|
||||||
|
resolution: {integrity: sha512-MW5hT4TyUp6bNAgTExRYLk1NNasVQMTCw1kgbxHcEC0O5cbepPWaB+1k+JzW9r3SP2/R8kiens8/3E6hGKfgsA==}
|
||||||
|
|
||||||
'@shikijs/types@3.4.2':
|
'@shikijs/types@3.4.2':
|
||||||
resolution: {integrity: sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg==}
|
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':
|
'@shikijs/vscode-textmate@10.0.2':
|
||||||
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
|
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
|
||||||
|
|
||||||
@@ -1938,6 +1981,9 @@ packages:
|
|||||||
engines: {node: '>= 8.0.0'}
|
engines: {node: '>= 8.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.0.0':
|
||||||
|
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
|
||||||
|
|
||||||
'@swc/helpers@0.5.17':
|
'@swc/helpers@0.5.17':
|
||||||
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
|
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
|
||||||
|
|
||||||
@@ -2223,12 +2269,12 @@ packages:
|
|||||||
'@types/debug@4.1.12':
|
'@types/debug@4.1.12':
|
||||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
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':
|
'@types/dom-to-image@2.6.7':
|
||||||
resolution: {integrity: sha512-me5VbCv+fcXozblWwG13krNBvuEOm6kA5xoa4RrjDJCNFOZSWR3/QLtOXimBHk1Fisq69Gx3JtOoXtg1N1tijg==}
|
resolution: {integrity: sha512-me5VbCv+fcXozblWwG13krNBvuEOm6kA5xoa4RrjDJCNFOZSWR3/QLtOXimBHk1Fisq69Gx3JtOoXtg1N1tijg==}
|
||||||
|
|
||||||
|
'@types/estree-jsx@1.0.5':
|
||||||
|
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
|
||||||
|
|
||||||
'@types/estree@1.0.7':
|
'@types/estree@1.0.7':
|
||||||
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
|
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
|
||||||
|
|
||||||
@@ -2241,6 +2287,9 @@ packages:
|
|||||||
'@types/js-cookie@3.0.6':
|
'@types/js-cookie@3.0.6':
|
||||||
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
|
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
|
||||||
|
|
||||||
|
'@types/katex@0.16.7':
|
||||||
|
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
|
||||||
|
|
||||||
'@types/linkify-it@3.0.5':
|
'@types/linkify-it@3.0.5':
|
||||||
resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==}
|
resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==}
|
||||||
|
|
||||||
@@ -2309,6 +2358,9 @@ packages:
|
|||||||
'@types/turndown@5.0.5':
|
'@types/turndown@5.0.5':
|
||||||
resolution: {integrity: sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w==}
|
resolution: {integrity: sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w==}
|
||||||
|
|
||||||
|
'@types/unist@2.0.11':
|
||||||
|
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
|
||||||
|
|
||||||
'@types/unist@3.0.3':
|
'@types/unist@3.0.3':
|
||||||
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
||||||
|
|
||||||
@@ -2346,15 +2398,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
|
resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
|
||||||
engines: {node: '>= 8.0.0'}
|
engines: {node: '>= 8.0.0'}
|
||||||
|
|
||||||
ai@4.3.16:
|
ai@5.0.0-beta.34:
|
||||||
resolution: {integrity: sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g==}
|
resolution: {integrity: sha512-AFJ4p35AxA+1KFtnoouePLaAUpoj0IxIAoq/xgIv88qzYajTg4Sac5KaV4CDHFRLoF0L2cwhlFXt/Ss/zyBKkA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18 || ^19 || ^19.0.0-rc
|
zod: ^3.25.76 || ^4
|
||||||
zod: ^3.23.8
|
|
||||||
peerDependenciesMeta:
|
|
||||||
react:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
ansi-align@3.0.1:
|
ansi-align@3.0.1:
|
||||||
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
|
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
|
||||||
@@ -2506,6 +2554,9 @@ packages:
|
|||||||
character-entities@2.0.2:
|
character-entities@2.0.2:
|
||||||
resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
|
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:
|
chokidar@4.0.3:
|
||||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||||
engines: {node: '>= 14.16.0'}
|
engines: {node: '>= 14.16.0'}
|
||||||
@@ -2565,6 +2616,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
commander@8.3.0:
|
||||||
|
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
|
||||||
|
engines: {node: '>= 12'}
|
||||||
|
|
||||||
common-ancestor-path@1.0.1:
|
common-ancestor-path@1.0.1:
|
||||||
resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==}
|
resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==}
|
||||||
|
|
||||||
@@ -2734,9 +2789,6 @@ packages:
|
|||||||
dfa@1.2.0:
|
dfa@1.2.0:
|
||||||
resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==}
|
resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==}
|
||||||
|
|
||||||
diff-match-patch@1.0.5:
|
|
||||||
resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
|
|
||||||
|
|
||||||
diff@5.2.0:
|
diff@5.2.0:
|
||||||
resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==}
|
resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==}
|
||||||
engines: {node: '>=0.3.1'}
|
engines: {node: '>=0.3.1'}
|
||||||
@@ -2864,6 +2916,9 @@ packages:
|
|||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
estree-util-is-identifier-name@3.0.0:
|
||||||
|
resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==}
|
||||||
|
|
||||||
estree-walker@2.0.2:
|
estree-walker@2.0.2:
|
||||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||||
|
|
||||||
@@ -2881,6 +2936,10 @@ packages:
|
|||||||
eventemitter3@5.0.1:
|
eventemitter3@5.0.1:
|
||||||
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
|
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:
|
extend-shallow@2.0.1:
|
||||||
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
|
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -3052,6 +3111,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
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:
|
hast-util-from-html@2.0.3:
|
||||||
resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==}
|
resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==}
|
||||||
|
|
||||||
@@ -3070,6 +3135,9 @@ packages:
|
|||||||
hast-util-to-html@9.0.5:
|
hast-util-to-html@9.0.5:
|
||||||
resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
|
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:
|
hast-util-to-parse5@8.0.0:
|
||||||
resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==}
|
resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==}
|
||||||
|
|
||||||
@@ -3096,6 +3164,9 @@ packages:
|
|||||||
html-escaper@3.0.3:
|
html-escaper@3.0.3:
|
||||||
resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==}
|
resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==}
|
||||||
|
|
||||||
|
html-url-attributes@3.0.1:
|
||||||
|
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
|
||||||
|
|
||||||
html-void-elements@3.0.0:
|
html-void-elements@3.0.0:
|
||||||
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
||||||
|
|
||||||
@@ -3127,6 +3198,9 @@ packages:
|
|||||||
inherits@2.0.4:
|
inherits@2.0.4:
|
||||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||||
|
|
||||||
|
inline-style-parser@0.2.4:
|
||||||
|
resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==}
|
||||||
|
|
||||||
iron-webcrypto@1.2.1:
|
iron-webcrypto@1.2.1:
|
||||||
resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==}
|
resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==}
|
||||||
|
|
||||||
@@ -3134,9 +3208,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==}
|
resolution: {integrity: sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
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:
|
is-arrayish@0.3.2:
|
||||||
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
|
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
|
||||||
|
|
||||||
|
is-decimal@2.0.1:
|
||||||
|
resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
|
||||||
|
|
||||||
is-docker@3.0.0:
|
is-docker@3.0.0:
|
||||||
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
|
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
@@ -3158,6 +3241,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
is-hexadecimal@2.0.1:
|
||||||
|
resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
|
||||||
|
|
||||||
is-inside-container@1.0.0:
|
is-inside-container@1.0.0:
|
||||||
resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
|
resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
|
||||||
engines: {node: '>=14.16'}
|
engines: {node: '>=14.16'}
|
||||||
@@ -3224,14 +3310,13 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
hasBin: true
|
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:
|
jsonfile@6.1.0:
|
||||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||||
|
|
||||||
|
katex@0.16.22:
|
||||||
|
resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
kind-of@6.0.3:
|
kind-of@6.0.3:
|
||||||
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
|
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -3486,6 +3571,18 @@ packages:
|
|||||||
mdast-util-gfm@3.1.0:
|
mdast-util-gfm@3.1.0:
|
||||||
resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==}
|
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:
|
mdast-util-phrasing@4.1.0:
|
||||||
resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
|
resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
|
||||||
|
|
||||||
@@ -3535,6 +3632,9 @@ packages:
|
|||||||
micromark-extension-gfm@3.0.0:
|
micromark-extension-gfm@3.0.0:
|
||||||
resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==}
|
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:
|
micromark-factory-destination@2.0.1:
|
||||||
resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==}
|
resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==}
|
||||||
|
|
||||||
@@ -3776,6 +3876,9 @@ packages:
|
|||||||
parse-css-color@0.2.1:
|
parse-css-color@0.2.1:
|
||||||
resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==}
|
resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==}
|
||||||
|
|
||||||
|
parse-entities@4.0.2:
|
||||||
|
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
|
||||||
|
|
||||||
parse-latin@7.0.0:
|
parse-latin@7.0.0:
|
||||||
resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==}
|
resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==}
|
||||||
|
|
||||||
@@ -4065,6 +4168,12 @@ packages:
|
|||||||
react-is@16.13.1:
|
react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
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:
|
react-refresh@0.17.0:
|
||||||
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
|
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -4137,6 +4246,9 @@ packages:
|
|||||||
rehype-external-links@3.0.0:
|
rehype-external-links@3.0.0:
|
||||||
resolution: {integrity: sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==}
|
resolution: {integrity: sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==}
|
||||||
|
|
||||||
|
rehype-katex@7.0.1:
|
||||||
|
resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==}
|
||||||
|
|
||||||
rehype-parse@9.0.1:
|
rehype-parse@9.0.1:
|
||||||
resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==}
|
resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==}
|
||||||
|
|
||||||
@@ -4152,6 +4264,9 @@ packages:
|
|||||||
remark-gfm@4.0.1:
|
remark-gfm@4.0.1:
|
||||||
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
|
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
|
||||||
|
|
||||||
|
remark-math@6.0.0:
|
||||||
|
resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==}
|
||||||
|
|
||||||
remark-parse@11.0.0:
|
remark-parse@11.0.0:
|
||||||
resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==}
|
resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==}
|
||||||
|
|
||||||
@@ -4354,6 +4469,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==}
|
resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==}
|
||||||
engines: {node: '>=0.10.0'}
|
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:
|
sucrase@3.35.0:
|
||||||
resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
|
resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
@@ -4362,8 +4483,8 @@ packages:
|
|||||||
suf-log@2.5.3:
|
suf-log@2.5.3:
|
||||||
resolution: {integrity: sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==}
|
resolution: {integrity: sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==}
|
||||||
|
|
||||||
swr@2.3.3:
|
swr@2.3.5:
|
||||||
resolution: {integrity: sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==}
|
resolution: {integrity: sha512-4e7pjTVulZTIL+b/S0RYFsgDcTcXPLUOvBPqyh9YdD+PkHeEMoaPwDmF9Kv6I1nnPg1OFKhiiEYpsYaaE2W2jA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
@@ -4830,6 +4951,9 @@ packages:
|
|||||||
zod@3.24.4:
|
zod@3.24.4:
|
||||||
resolution: {integrity: sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==}
|
resolution: {integrity: sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==}
|
||||||
|
|
||||||
|
zod@4.0.17:
|
||||||
|
resolution: {integrity: sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==}
|
||||||
|
|
||||||
zustand@4.5.6:
|
zustand@4.5.6:
|
||||||
resolution: {integrity: sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==}
|
resolution: {integrity: sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==}
|
||||||
engines: {node: '>=12.7.0'}
|
engines: {node: '>=12.7.0'}
|
||||||
@@ -4868,39 +4992,50 @@ packages:
|
|||||||
|
|
||||||
snapshots:
|
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:
|
dependencies:
|
||||||
'@ai-sdk/provider': 1.1.3
|
'@ai-sdk/provider': 1.1.3
|
||||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.24.4)
|
'@ai-sdk/provider-utils': 2.2.8(zod@4.0.17)
|
||||||
zod: 3.24.4
|
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:
|
dependencies:
|
||||||
'@ai-sdk/provider': 1.1.3
|
'@ai-sdk/provider': 1.1.3
|
||||||
nanoid: 3.3.11
|
nanoid: 3.3.11
|
||||||
secure-json-parse: 2.7.0
|
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':
|
'@ai-sdk/provider@1.1.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
json-schema: 0.4.0
|
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:
|
dependencies:
|
||||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.24.4)
|
json-schema: 0.4.0
|
||||||
'@ai-sdk/ui-utils': 1.2.11(zod@3.24.4)
|
|
||||||
|
'@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
|
react: 19.1.0
|
||||||
swr: 2.3.3(react@19.1.0)
|
swr: 2.3.5(react@19.1.0)
|
||||||
throttleit: 2.1.0
|
throttleit: 2.1.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
zod: 3.24.4
|
zod: 4.0.17
|
||||||
|
|
||||||
'@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)
|
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
|
|
||||||
@@ -6377,6 +6512,13 @@ snapshots:
|
|||||||
'@types/hast': 3.0.4
|
'@types/hast': 3.0.4
|
||||||
hast-util-to-html: 9.0.5
|
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':
|
'@shikijs/engine-javascript@3.4.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@shikijs/types': 3.4.2
|
'@shikijs/types': 3.4.2
|
||||||
@@ -6396,11 +6538,21 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@shikijs/types': 3.4.2
|
'@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':
|
'@shikijs/types@3.4.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@shikijs/vscode-textmate': 10.0.2
|
'@shikijs/vscode-textmate': 10.0.2
|
||||||
'@types/hast': 3.0.4
|
'@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': {}
|
'@shikijs/vscode-textmate@10.0.2': {}
|
||||||
|
|
||||||
'@shuding/opentype.js@1.4.0-beta.0':
|
'@shuding/opentype.js@1.4.0-beta.0':
|
||||||
@@ -6408,6 +6560,8 @@ snapshots:
|
|||||||
fflate: 0.7.4
|
fflate: 0.7.4
|
||||||
string.prototype.codepointat: 0.2.1
|
string.prototype.codepointat: 0.2.1
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.0.0': {}
|
||||||
|
|
||||||
'@swc/helpers@0.5.17':
|
'@swc/helpers@0.5.17':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@@ -6686,10 +6840,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/ms': 2.1.0
|
'@types/ms': 2.1.0
|
||||||
|
|
||||||
'@types/diff-match-patch@1.0.36': {}
|
|
||||||
|
|
||||||
'@types/dom-to-image@2.6.7': {}
|
'@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/estree@1.0.7': {}
|
||||||
|
|
||||||
'@types/fontkit@2.0.8':
|
'@types/fontkit@2.0.8':
|
||||||
@@ -6702,6 +6858,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/js-cookie@3.0.6': {}
|
'@types/js-cookie@3.0.6': {}
|
||||||
|
|
||||||
|
'@types/katex@0.16.7': {}
|
||||||
|
|
||||||
'@types/linkify-it@3.0.5': {}
|
'@types/linkify-it@3.0.5': {}
|
||||||
|
|
||||||
'@types/linkify-it@5.0.0': {}
|
'@types/linkify-it@5.0.0': {}
|
||||||
@@ -6775,6 +6933,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/turndown@5.0.5': {}
|
'@types/turndown@5.0.5': {}
|
||||||
|
|
||||||
|
'@types/unist@2.0.11': {}
|
||||||
|
|
||||||
'@types/unist@3.0.3': {}
|
'@types/unist@3.0.3': {}
|
||||||
|
|
||||||
'@types/use-sync-external-store@0.0.6': {}
|
'@types/use-sync-external-store@0.0.6': {}
|
||||||
@@ -6823,17 +6983,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
humanize-ms: 1.2.1
|
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:
|
dependencies:
|
||||||
'@ai-sdk/provider': 1.1.3
|
'@ai-sdk/gateway': 1.0.0-beta.19(zod@4.0.17)
|
||||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.24.4)
|
'@ai-sdk/provider': 2.0.0-beta.2
|
||||||
'@ai-sdk/react': 1.2.12(react@19.1.0)(zod@3.24.4)
|
'@ai-sdk/provider-utils': 3.0.0-beta.10(zod@4.0.17)
|
||||||
'@ai-sdk/ui-utils': 1.2.11(zod@3.24.4)
|
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
jsondiffpatch: 0.6.0
|
zod: 4.0.17
|
||||||
zod: 3.24.4
|
|
||||||
optionalDependencies:
|
|
||||||
react: 19.1.0
|
|
||||||
|
|
||||||
ansi-align@3.0.1:
|
ansi-align@3.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7053,6 +7209,8 @@ snapshots:
|
|||||||
|
|
||||||
character-entities@2.0.2: {}
|
character-entities@2.0.2: {}
|
||||||
|
|
||||||
|
character-reference-invalid@2.0.1: {}
|
||||||
|
|
||||||
chokidar@4.0.3:
|
chokidar@4.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
readdirp: 4.1.2
|
readdirp: 4.1.2
|
||||||
@@ -7097,6 +7255,8 @@ snapshots:
|
|||||||
|
|
||||||
commander@4.1.1: {}
|
commander@4.1.1: {}
|
||||||
|
|
||||||
|
commander@8.3.0: {}
|
||||||
|
|
||||||
common-ancestor-path@1.0.1: {}
|
common-ancestor-path@1.0.1: {}
|
||||||
|
|
||||||
commondir@1.0.1: {}
|
commondir@1.0.1: {}
|
||||||
@@ -7236,8 +7396,6 @@ snapshots:
|
|||||||
|
|
||||||
dfa@1.2.0: {}
|
dfa@1.2.0: {}
|
||||||
|
|
||||||
diff-match-patch@1.0.5: {}
|
|
||||||
|
|
||||||
diff@5.2.0: {}
|
diff@5.2.0: {}
|
||||||
|
|
||||||
dir-glob@3.0.1:
|
dir-glob@3.0.1:
|
||||||
@@ -7360,6 +7518,8 @@ snapshots:
|
|||||||
|
|
||||||
esprima@4.0.1: {}
|
esprima@4.0.1: {}
|
||||||
|
|
||||||
|
estree-util-is-identifier-name@3.0.0: {}
|
||||||
|
|
||||||
estree-walker@2.0.2: {}
|
estree-walker@2.0.2: {}
|
||||||
|
|
||||||
estree-walker@3.0.3:
|
estree-walker@3.0.3:
|
||||||
@@ -7372,6 +7532,8 @@ snapshots:
|
|||||||
|
|
||||||
eventemitter3@5.0.1: {}
|
eventemitter3@5.0.1: {}
|
||||||
|
|
||||||
|
eventsource-parser@3.0.3: {}
|
||||||
|
|
||||||
extend-shallow@2.0.1:
|
extend-shallow@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-extendable: 0.1.1
|
is-extendable: 0.1.1
|
||||||
@@ -7576,6 +7738,19 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
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:
|
hast-util-from-html@2.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/hast': 3.0.4
|
'@types/hast': 3.0.4
|
||||||
@@ -7634,6 +7809,26 @@ snapshots:
|
|||||||
stringify-entities: 4.0.4
|
stringify-entities: 4.0.4
|
||||||
zwitch: 2.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:
|
hast-util-to-parse5@8.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/hast': 3.0.4
|
'@types/hast': 3.0.4
|
||||||
@@ -7671,6 +7866,8 @@ snapshots:
|
|||||||
|
|
||||||
html-escaper@3.0.3: {}
|
html-escaper@3.0.3: {}
|
||||||
|
|
||||||
|
html-url-attributes@3.0.1: {}
|
||||||
|
|
||||||
html-void-elements@3.0.0: {}
|
html-void-elements@3.0.0: {}
|
||||||
|
|
||||||
htmlparser2@8.0.2:
|
htmlparser2@8.0.2:
|
||||||
@@ -7702,12 +7899,23 @@ snapshots:
|
|||||||
|
|
||||||
inherits@2.0.4: {}
|
inherits@2.0.4: {}
|
||||||
|
|
||||||
|
inline-style-parser@0.2.4: {}
|
||||||
|
|
||||||
iron-webcrypto@1.2.1: {}
|
iron-webcrypto@1.2.1: {}
|
||||||
|
|
||||||
is-absolute-url@4.0.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-arrayish@0.3.2: {}
|
||||||
|
|
||||||
|
is-decimal@2.0.1: {}
|
||||||
|
|
||||||
is-docker@3.0.0: {}
|
is-docker@3.0.0: {}
|
||||||
|
|
||||||
is-extendable@0.1.1: {}
|
is-extendable@0.1.1: {}
|
||||||
@@ -7720,6 +7928,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-extglob: 2.1.1
|
is-extglob: 2.1.1
|
||||||
|
|
||||||
|
is-hexadecimal@2.0.1: {}
|
||||||
|
|
||||||
is-inside-container@1.0.0:
|
is-inside-container@1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-docker: 3.0.0
|
is-docker: 3.0.0
|
||||||
@@ -7767,18 +7977,16 @@ snapshots:
|
|||||||
|
|
||||||
json5@2.2.3: {}
|
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:
|
jsonfile@6.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
universalify: 2.0.1
|
universalify: 2.0.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
|
|
||||||
|
katex@0.16.22:
|
||||||
|
dependencies:
|
||||||
|
commander: 8.3.0
|
||||||
|
|
||||||
kind-of@6.0.3: {}
|
kind-of@6.0.3: {}
|
||||||
|
|
||||||
kleur@3.0.3: {}
|
kleur@3.0.3: {}
|
||||||
@@ -8045,6 +8253,57 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
mdast-util-phrasing@4.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/mdast': 4.0.4
|
'@types/mdast': 4.0.4
|
||||||
@@ -8163,6 +8422,16 @@ snapshots:
|
|||||||
micromark-util-combine-extensions: 2.0.1
|
micromark-util-combine-extensions: 2.0.1
|
||||||
micromark-util-types: 2.0.2
|
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:
|
micromark-factory-destination@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
micromark-util-character: 2.1.1
|
micromark-util-character: 2.1.1
|
||||||
@@ -8377,7 +8646,7 @@ snapshots:
|
|||||||
regex: 6.0.1
|
regex: 6.0.1
|
||||||
regex-recursion: 6.0.2
|
regex-recursion: 6.0.2
|
||||||
|
|
||||||
openai@4.100.0(zod@3.24.4):
|
openai@4.100.0(zod@4.0.17):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 18.19.100
|
'@types/node': 18.19.100
|
||||||
'@types/node-fetch': 2.6.12
|
'@types/node-fetch': 2.6.12
|
||||||
@@ -8387,7 +8656,7 @@ snapshots:
|
|||||||
formdata-node: 4.4.1
|
formdata-node: 4.4.1
|
||||||
node-fetch: 2.7.0
|
node-fetch: 2.7.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
zod: 3.24.4
|
zod: 4.0.17
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
|
|
||||||
@@ -8425,6 +8694,16 @@ snapshots:
|
|||||||
color-name: 1.1.4
|
color-name: 1.1.4
|
||||||
hex-rgb: 4.3.0
|
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:
|
parse-latin@7.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/nlcst': 2.0.3
|
'@types/nlcst': 2.0.3
|
||||||
@@ -8736,6 +9015,24 @@ snapshots:
|
|||||||
|
|
||||||
react-is@16.13.1: {}
|
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-refresh@0.17.0: {}
|
||||||
|
|
||||||
react-remove-scroll-bar@2.3.8(@types/react@19.1.4)(react@19.1.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
|
space-separated-tokens: 2.0.2
|
||||||
unist-util-visit: 5.0.0
|
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:
|
rehype-parse@9.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/hast': 3.0.4
|
'@types/hast': 3.0.4
|
||||||
@@ -8845,6 +9152,15 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
remark-parse@11.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/mdast': 4.0.4
|
'@types/mdast': 4.0.4
|
||||||
@@ -9155,6 +9471,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
escape-string-regexp: 1.0.5
|
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:
|
sucrase@3.35.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/gen-mapping': 0.3.8
|
'@jridgewell/gen-mapping': 0.3.8
|
||||||
@@ -9169,7 +9493,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
s.color: 0.0.15
|
s.color: 0.0.15
|
||||||
|
|
||||||
swr@2.3.3(react@19.1.0):
|
swr@2.3.5(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
dequal: 2.0.3
|
dequal: 2.0.3
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
@@ -9547,6 +9871,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
zod: 3.24.4
|
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):
|
zod-to-ts@1.2.0(typescript@5.8.3)(zod@3.24.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
@@ -9554,6 +9882,8 @@ snapshots:
|
|||||||
|
|
||||||
zod@3.24.4: {}
|
zod@3.24.4: {}
|
||||||
|
|
||||||
|
zod@4.0.17: {}
|
||||||
|
|
||||||
zustand@4.5.6(@types/react@19.1.4)(react@19.1.0):
|
zustand@4.5.6(@types/react@19.1.4)(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
use-sync-external-store: 1.5.0(react@19.1.0)
|
use-sync-external-store: 1.5.0(react@19.1.0)
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,3 +1,5 @@
|
|||||||
|
import '../ChatMessages/AIChat.css';
|
||||||
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import type { JSONContent } from '@tiptap/core';
|
import type { JSONContent } from '@tiptap/core';
|
||||||
import {
|
import {
|
||||||
@@ -14,13 +16,9 @@ import {
|
|||||||
Wand2,
|
Wand2,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { flushSync } from 'react-dom';
|
import { flushSync } from 'react-dom';
|
||||||
import { useKeydown } from '../../hooks/use-keydown';
|
import { useKeydown } from '../../hooks/use-keydown';
|
||||||
import {
|
|
||||||
roadmapAIChatRenderer,
|
|
||||||
useRoadmapAIChat,
|
|
||||||
} from '../../hooks/use-roadmap-ai-chat';
|
|
||||||
import { cn } from '../../lib/classname';
|
import { cn } from '../../lib/classname';
|
||||||
import { lockBodyScroll } from '../../lib/dom';
|
import { lockBodyScroll } from '../../lib/dom';
|
||||||
import { isLoggedIn } from '../../lib/jwt';
|
import { isLoggedIn } from '../../lib/jwt';
|
||||||
@@ -33,10 +31,14 @@ import { roadmapJSONOptions } from '../../queries/roadmap';
|
|||||||
import { roadmapQuestionsOptions } from '../../queries/roadmap-questions';
|
import { roadmapQuestionsOptions } from '../../queries/roadmap-questions';
|
||||||
import { queryClient } from '../../stores/query-client';
|
import { queryClient } from '../../stores/query-client';
|
||||||
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
|
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
|
||||||
import { RoadmapAIChatCard } from '../RoadmapAIChat/RoadmapAIChatCard';
|
|
||||||
import { RoadmapAIChatHistory } from '../RoadmapAIChatHistory/RoadmapAIChatHistory';
|
import { RoadmapAIChatHistory } from '../RoadmapAIChatHistory/RoadmapAIChatHistory';
|
||||||
import { CLOSE_TOPIC_DETAIL_EVENT } from '../TopicDetail/TopicDetail';
|
import { CLOSE_TOPIC_DETAIL_EVENT } from '../TopicDetail/TopicDetail';
|
||||||
import { UpdatePersonaModal } from '../UserPersona/UpdatePersonaModal';
|
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 = {
|
type ChatHeaderButtonProps = {
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
@@ -158,10 +160,12 @@ type RoadmapChatProps = {
|
|||||||
|
|
||||||
export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
||||||
const { roadmapId } = props;
|
const { roadmapId } = props;
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const scrollareaRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const [isPersonalizeOpen, setIsPersonalizeOpen] = useState(false);
|
const [isPersonalizeOpen, setIsPersonalizeOpen] = useState(false);
|
||||||
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
|
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
|
||||||
|
|
||||||
@@ -176,9 +180,7 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
|||||||
if (!questionsData?.questions || questionsData.questions.length === 0) {
|
if (!questionsData?.questions || questionsData.questions.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const shuffled = [...questionsData.questions].sort(
|
const shuffled = shuffle([...questionsData.questions]);
|
||||||
() => 0.5 - Math.random(),
|
|
||||||
);
|
|
||||||
return shuffled.slice(0, 4);
|
return shuffled.slice(0, 4);
|
||||||
}, [questionsData]);
|
}, [questionsData]);
|
||||||
|
|
||||||
@@ -236,45 +238,36 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
|||||||
string | undefined
|
string | undefined
|
||||||
>();
|
>();
|
||||||
const { data: chatHistory } = useQuery(
|
const { data: chatHistory } = useQuery(
|
||||||
chatHistoryOptions(
|
chatHistoryOptions(activeChatHistoryId),
|
||||||
activeChatHistoryId,
|
|
||||||
roadmapAIChatRenderer({
|
|
||||||
roadmapId,
|
|
||||||
totalTopicCount,
|
|
||||||
onSelectTopic,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
queryClient,
|
queryClient,
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const { messages, sendMessage, status, stop, setMessages } = useChat({
|
||||||
aiChatHistory,
|
transport: chatRoadmapTransport,
|
||||||
isStreamingMessage,
|
onData: (data) => {
|
||||||
streamedMessage,
|
if (data.type === 'data-redirect') {
|
||||||
showScrollToBottom,
|
const { title, chatId } = data.data as {
|
||||||
setShowScrollToBottom,
|
title: string;
|
||||||
handleChatSubmit,
|
chatId: string;
|
||||||
handleAbort,
|
};
|
||||||
scrollToBottom,
|
|
||||||
clearChat,
|
document.title = title;
|
||||||
setAiChatHistory,
|
setActiveChatHistoryId(chatId);
|
||||||
} = useRoadmapAIChat({
|
}
|
||||||
activeChatHistoryId,
|
|
||||||
roadmapId,
|
|
||||||
totalTopicCount,
|
|
||||||
scrollareaRef,
|
|
||||||
onSelectTopic,
|
|
||||||
onChatHistoryIdChange: (chatHistoryId) => {
|
|
||||||
setActiveChatHistoryId(chatHistoryId);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { scrollToBottom, scrollableContainerRef, showScrollToBottomButton } =
|
||||||
|
useAIChatScroll({
|
||||||
|
messages,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chatHistory) {
|
if (!chatHistory) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setAiChatHistory(chatHistory?.messages ?? []);
|
setMessages(chatHistory?.messages ?? []);
|
||||||
setIsChatHistoryLoading(false);
|
setIsChatHistoryLoading(false);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
scrollToBottom('instant');
|
scrollToBottom('instant');
|
||||||
@@ -286,9 +279,9 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setAiChatHistory([]);
|
setMessages([]);
|
||||||
setIsChatHistoryLoading(false);
|
setIsChatHistoryLoading(false);
|
||||||
}, [activeChatHistoryId, setAiChatHistory, setIsChatHistoryLoading]);
|
}, [activeChatHistoryId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
lockBodyScroll(isOpen);
|
lockBodyScroll(isOpen);
|
||||||
@@ -320,26 +313,45 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitInput = () => {
|
const clearChat = () => {
|
||||||
|
setMessages([]);
|
||||||
|
setInputValue('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitInput = (message?: string) => {
|
||||||
if (!isLoggedIn()) {
|
if (!isLoggedIn()) {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
showLoginPopup();
|
showLoginPopup();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const trimmed = inputValue.trim();
|
const trimmed = (message ?? inputValue).trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const json: JSONContent = textToJSON(trimmed);
|
sendMessage(
|
||||||
|
{ text: trimmed, metadata: { json: textToJSON(trimmed) } },
|
||||||
|
{
|
||||||
|
body: {
|
||||||
|
roadmapId,
|
||||||
|
...(activeChatHistoryId
|
||||||
|
? { chatHistoryId: activeChatHistoryId }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToBottom('smooth');
|
||||||
setInputValue('');
|
setInputValue('');
|
||||||
handleChatSubmit(json, isRoadmapDetailLoading);
|
inputRef.current?.focus();
|
||||||
|
}, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasMessages = aiChatHistory.length > 0;
|
const isStreamingMessage = status !== 'ready';
|
||||||
const newTabUrl = `/${roadmapId}/ai${activeChatHistoryId ? `?chatId=${activeChatHistoryId}` : ''}`;
|
const hasMessages = messages.length > 0;
|
||||||
|
const newTabUrl = `/ai/r/${roadmapId}${activeChatHistoryId ? `?chatId=${activeChatHistoryId}` : ''}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -371,7 +383,7 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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',
|
isOpen ? 'z-91 h-full w-full' : 'z-40 w-auto',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -417,7 +429,6 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
|||||||
onChatHistoryClick={(chatHistoryId) => {
|
onChatHistoryClick={(chatHistoryId) => {
|
||||||
setIsChatHistoryLoading(true);
|
setIsChatHistoryLoading(true);
|
||||||
setActiveChatHistoryId(chatHistoryId);
|
setActiveChatHistoryId(chatHistoryId);
|
||||||
setShowScrollToBottom(false);
|
|
||||||
}}
|
}}
|
||||||
onDelete={(chatHistoryId) => {
|
onDelete={(chatHistoryId) => {
|
||||||
if (activeChatHistoryId === chatHistoryId) {
|
if (activeChatHistoryId === chatHistoryId) {
|
||||||
@@ -443,82 +454,27 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="relative flex grow flex-col">
|
||||||
<div
|
<div
|
||||||
className="flex flex-1 flex-grow flex-col overflow-y-auto px-3 py-2"
|
className="relative grow overflow-y-auto"
|
||||||
ref={scrollareaRef}
|
ref={scrollableContainerRef}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-2 text-sm">
|
<RoadmapChatMessages
|
||||||
<RoadmapAIChatCard
|
messages={messages}
|
||||||
role="assistant"
|
status={status}
|
||||||
jsx={
|
roadmapId={roadmapId}
|
||||||
<span className="mt-[2px]">
|
defaultQuestions={defaultQuestions}
|
||||||
Hey, I am your AI tutor. How can I help you today? 👋
|
onTopicClick={onSelectTopic}
|
||||||
</span>
|
onDefaultQuestionClick={submitInput}
|
||||||
}
|
|
||||||
isIntro
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 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>
|
</div>
|
||||||
|
|
||||||
{/* Scroll to bottom button */}
|
{showScrollToBottomButton && (
|
||||||
{showScrollToBottom && (
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
scrollToBottom('instant');
|
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" />
|
<ChevronDown className="h-3 w-3" />
|
||||||
Scroll to bottom
|
Scroll to bottom
|
||||||
@@ -534,6 +490,7 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLimitExceeded && (
|
{!isLimitExceeded && (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-row justify-between border-t border-gray-200 px-3 pt-2">
|
<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) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (isStreamingMessage) {
|
if (status !== 'ready') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
submitInput();
|
submitInput();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -609,9 +567,10 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
|||||||
disabled={isRoadmapDetailLoading || isLimitExceeded}
|
disabled={isRoadmapDetailLoading || isLimitExceeded}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isStreamingMessage) {
|
if (isStreamingMessage) {
|
||||||
handleAbort();
|
stop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
submitInput();
|
submitInput();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -637,7 +596,6 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
|||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
scrollToBottom('instant');
|
scrollToBottom('instant');
|
||||||
setShowScrollToBottom(false);
|
|
||||||
}, 0);
|
}, 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;
|
||||||
|
});
|
@@ -25,9 +25,7 @@ import { Ban, FileText, HeartHandshake, Star, X } from 'lucide-react';
|
|||||||
import { getUrlParams, parseUrl } from '../../lib/browser';
|
import { getUrlParams, parseUrl } from '../../lib/browser';
|
||||||
import { Spinner } from '../ReactIcons/Spinner';
|
import { Spinner } from '../ReactIcons/Spinner';
|
||||||
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
|
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
|
||||||
import {
|
import { type AllowedRoadmapRenderer } from '../../lib/roadmap.ts';
|
||||||
type AllowedRoadmapRenderer
|
|
||||||
} from '../../lib/roadmap.ts';
|
|
||||||
import { lockBodyScroll } from '../../lib/dom.ts';
|
import { lockBodyScroll } from '../../lib/dom.ts';
|
||||||
import { TopicDetailLink } from './TopicDetailLink.tsx';
|
import { TopicDetailLink } from './TopicDetailLink.tsx';
|
||||||
import { ResourceListSeparator } from './ResourceListSeparator.tsx';
|
import { ResourceListSeparator } from './ResourceListSeparator.tsx';
|
||||||
@@ -42,6 +40,8 @@ import type { AIChatHistoryType } from '../GenerateCourse/AICourseLessonChat.tsx
|
|||||||
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal.tsx';
|
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal.tsx';
|
||||||
import { TopicProgressButton } from './TopicProgressButton.tsx';
|
import { TopicProgressButton } from './TopicProgressButton.tsx';
|
||||||
import { CreateCourseModal } from './CreateCourseModal.tsx';
|
import { CreateCourseModal } from './CreateCourseModal.tsx';
|
||||||
|
import { useChat } from '@ai-sdk/react';
|
||||||
|
import { topicDetailAiChatTransport } from '../../lib/ai.ts';
|
||||||
|
|
||||||
type PaidResourceType = {
|
type PaidResourceType = {
|
||||||
_id?: string;
|
_id?: string;
|
||||||
@@ -134,8 +134,6 @@ export function TopicDetail(props: TopicDetailProps) {
|
|||||||
const [links, setLinks] = useState<RoadmapContentDocument['links']>([]);
|
const [links, setLinks] = useState<RoadmapContentDocument['links']>([]);
|
||||||
const [activeTab, setActiveTab] =
|
const [activeTab, setActiveTab] =
|
||||||
useState<AllowedTopicDetailsTabs>(defaultActiveTab);
|
useState<AllowedTopicDetailsTabs>(defaultActiveTab);
|
||||||
const [aiChatHistory, setAiChatHistory] =
|
|
||||||
useState<AIChatHistoryType[]>(defaultChatHistory);
|
|
||||||
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
|
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
|
||||||
const [isCustomResource, setIsCustomResource] = useState(false);
|
const [isCustomResource, setIsCustomResource] = useState(false);
|
||||||
|
|
||||||
@@ -156,14 +154,20 @@ export function TopicDetail(props: TopicDetailProps) {
|
|||||||
const [resourceType, setResourceType] = useState<ResourceType>('roadmap');
|
const [resourceType, setResourceType] = useState<ResourceType>('roadmap');
|
||||||
const [paidResources, setPaidResources] = useState<PaidResourceType[]>([]);
|
const [paidResources, setPaidResources] = useState<PaidResourceType[]>([]);
|
||||||
|
|
||||||
|
const chatId = `${resourceType}-${resourceId}-${topicId}`;
|
||||||
|
const { messages, sendMessage, setMessages, status } = useChat({
|
||||||
|
id: chatId,
|
||||||
|
transport: topicDetailAiChatTransport,
|
||||||
|
});
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
onClose?.();
|
onClose?.();
|
||||||
setIsActive(false);
|
setIsActive(false);
|
||||||
setIsContributing(false);
|
setIsContributing(false);
|
||||||
setShowUpgradeModal(false);
|
setShowUpgradeModal(false);
|
||||||
setAiChatHistory(defaultChatHistory);
|
|
||||||
setActiveTab('content');
|
setActiveTab('content');
|
||||||
setShowSubjectSearchModal(false);
|
setShowSubjectSearchModal(false);
|
||||||
|
setMessages([]);
|
||||||
|
|
||||||
lockBodyScroll(false);
|
lockBodyScroll(false);
|
||||||
|
|
||||||
@@ -485,8 +489,10 @@ export function TopicDetail(props: TopicDetailProps) {
|
|||||||
resourceId={resourceId}
|
resourceId={resourceId}
|
||||||
resourceType={resourceType}
|
resourceType={resourceType}
|
||||||
topicId={topicId}
|
topicId={topicId}
|
||||||
aiChatHistory={aiChatHistory}
|
messages={messages}
|
||||||
setAiChatHistory={setAiChatHistory}
|
setMessages={setMessages}
|
||||||
|
status={status}
|
||||||
|
sendMessage={sendMessage}
|
||||||
hasUpgradeButtons={hasUpgradeButtons}
|
hasUpgradeButtons={hasUpgradeButtons}
|
||||||
onUpgrade={() => setShowUpgradeModal(true)}
|
onUpgrade={() => setShowUpgradeModal(true)}
|
||||||
onLogin={() => {
|
onLogin={() => {
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import '../ChatMessages/AIChat.css';
|
||||||
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
BotIcon,
|
BotIcon,
|
||||||
@@ -9,14 +11,11 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
WandSparkles,
|
WandSparkles,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Fragment, useCallback, useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { flushSync } from 'react-dom';
|
|
||||||
import TextareaAutosize from 'react-textarea-autosize';
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
import { useToast } from '../../hooks/use-toast';
|
import { useToast } from '../../hooks/use-toast';
|
||||||
import { readStream } from '../../lib/ai';
|
|
||||||
import { cn } from '../../lib/classname';
|
import { cn } from '../../lib/classname';
|
||||||
import { isLoggedIn, removeAuthToken } from '../../lib/jwt';
|
import { isLoggedIn } from '../../lib/jwt';
|
||||||
import { markdownToHtmlWithHighlighting } from '../../lib/markdown';
|
|
||||||
import { getPercentage } from '../../lib/number';
|
import { getPercentage } from '../../lib/number';
|
||||||
import { showLoginPopup } from '../../lib/popup';
|
import { showLoginPopup } from '../../lib/popup';
|
||||||
import type { ResourceType } from '../../lib/resource-progress';
|
import type { ResourceType } from '../../lib/resource-progress';
|
||||||
@@ -24,14 +23,12 @@ import { aiLimitOptions } from '../../queries/ai-course';
|
|||||||
import { billingDetailsOptions } from '../../queries/billing';
|
import { billingDetailsOptions } from '../../queries/billing';
|
||||||
import { roadmapTreeMappingOptions } from '../../queries/roadmap-tree';
|
import { roadmapTreeMappingOptions } from '../../queries/roadmap-tree';
|
||||||
import { queryClient } from '../../stores/query-client';
|
import { queryClient } from '../../stores/query-client';
|
||||||
import {
|
|
||||||
AIChatCard,
|
|
||||||
type AIChatHistoryType,
|
|
||||||
} from '../GenerateCourse/AICourseLessonChat';
|
|
||||||
import '../GenerateCourse/AICourseLessonChat.css';
|
|
||||||
import { AILimitsPopup } from '../GenerateCourse/AILimitsPopup';
|
import { AILimitsPopup } from '../GenerateCourse/AILimitsPopup';
|
||||||
import { PredefinedActions, promptLabelMapping } from './PredefinedActions';
|
import { PredefinedActions } from './PredefinedActions';
|
||||||
import { defaultChatHistory } from './TopicDetail';
|
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 = {
|
type TopicDetailAIProps = {
|
||||||
resourceId: string;
|
resourceId: string;
|
||||||
@@ -40,8 +37,10 @@ type TopicDetailAIProps = {
|
|||||||
|
|
||||||
hasUpgradeButtons?: boolean;
|
hasUpgradeButtons?: boolean;
|
||||||
|
|
||||||
aiChatHistory: AIChatHistoryType[];
|
messages: UIMessage[];
|
||||||
setAiChatHistory: (history: AIChatHistoryType[]) => void;
|
sendMessage: UseChatHelpers<UIMessage>['sendMessage'];
|
||||||
|
setMessages: UseChatHelpers<UIMessage>['setMessages'];
|
||||||
|
status: ChatStatus;
|
||||||
|
|
||||||
onUpgrade: () => void;
|
onUpgrade: () => void;
|
||||||
onLogin: () => void;
|
onLogin: () => void;
|
||||||
@@ -51,8 +50,11 @@ type TopicDetailAIProps = {
|
|||||||
|
|
||||||
export function TopicDetailAI(props: TopicDetailAIProps) {
|
export function TopicDetailAI(props: TopicDetailAIProps) {
|
||||||
const {
|
const {
|
||||||
aiChatHistory,
|
messages,
|
||||||
setAiChatHistory,
|
sendMessage,
|
||||||
|
setMessages,
|
||||||
|
status,
|
||||||
|
|
||||||
resourceId,
|
resourceId,
|
||||||
resourceType,
|
resourceType,
|
||||||
topicId,
|
topicId,
|
||||||
@@ -63,7 +65,6 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
|||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const scrollareaRef = useRef<HTMLDivElement>(null);
|
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
|
|
||||||
const sanitizedTopicId = topicId?.includes('@')
|
const sanitizedTopicId = topicId?.includes('@')
|
||||||
@@ -72,8 +73,6 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
|||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
const [isStreamingMessage, setIsStreamingMessage] = useState(false);
|
|
||||||
const [streamedMessage, setStreamedMessage] = useState('');
|
|
||||||
const [showAILimitsPopup, setShowAILimitsPopup] = useState(false);
|
const [showAILimitsPopup, setShowAILimitsPopup] = useState(false);
|
||||||
const { data: tokenUsage, isLoading } = useQuery(
|
const { data: tokenUsage, isLoading } = useQuery(
|
||||||
aiLimitOptions(),
|
aiLimitOptions(),
|
||||||
@@ -105,7 +104,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!trimmedMessage ||
|
!trimmedMessage ||
|
||||||
isStreamingMessage ||
|
status !== 'ready' ||
|
||||||
!isLoggedIn() ||
|
!isLoggedIn() ||
|
||||||
isLimitExceeded ||
|
isLimitExceeded ||
|
||||||
isLoading
|
isLoading
|
||||||
@@ -113,111 +112,31 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newMessages: AIChatHistoryType[] = [
|
sendMessage(
|
||||||
...aiChatHistory,
|
|
||||||
{
|
{
|
||||||
role: 'user',
|
text: trimmedMessage,
|
||||||
content: trimmedMessage,
|
|
||||||
},
|
},
|
||||||
];
|
|
||||||
|
|
||||||
flushSync(() => {
|
|
||||||
setAiChatHistory(newMessages);
|
|
||||||
setMessage('');
|
|
||||||
});
|
|
||||||
|
|
||||||
scrollToBottom();
|
|
||||||
completeAITutorChat(newMessages);
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrollToBottom = useCallback(() => {
|
|
||||||
scrollareaRef.current?.scrollTo({
|
|
||||||
top: scrollareaRef.current.scrollHeight,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
}, [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',
|
body: {
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
body: JSON.stringify({
|
|
||||||
resourceId,
|
resourceId,
|
||||||
resourceType,
|
resourceType,
|
||||||
topicId: sanitizedTopicId,
|
topicId: sanitizedTopicId,
|
||||||
messages: messages.slice(-10),
|
},
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
setMessage('');
|
||||||
const data = await response.json();
|
setTimeout(() => {
|
||||||
|
|
||||||
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();
|
scrollToBottom();
|
||||||
},
|
textareaRef.current?.focus();
|
||||||
onStreamEnd: async (content) => {
|
}, 0);
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { scrollToBottom, scrollableContainerRef, showScrollToBottomButton } =
|
||||||
|
useAIChatScroll({
|
||||||
|
messages,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -228,7 +147,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
|||||||
tokenUsage?.used || 0,
|
tokenUsage?.used || 0,
|
||||||
tokenUsage?.limit || 0,
|
tokenUsage?.limit || 0,
|
||||||
);
|
);
|
||||||
const hasChatHistory = aiChatHistory.length > 1;
|
const hasChatHistory = messages.length > 0;
|
||||||
const nodeTextParts = roadmapTreeMapping?.text?.split('>') || [];
|
const nodeTextParts = roadmapTreeMapping?.text?.split('>') || [];
|
||||||
const hasSubjects =
|
const hasSubjects =
|
||||||
(roadmapTreeMapping?.subjects &&
|
(roadmapTreeMapping?.subjects &&
|
||||||
@@ -236,7 +155,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
|||||||
nodeTextParts.length > 1;
|
nodeTextParts.length > 1;
|
||||||
|
|
||||||
return (
|
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 && (
|
{isDataLoading && (
|
||||||
<div className="absolute inset-0 z-20 flex items-center justify-center gap-2 bg-white text-black">
|
<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" />
|
<Loader2Icon className="size-8 animate-spin stroke-3 text-gray-500" />
|
||||||
@@ -279,7 +198,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
href={`/ai/course?term=${subject}&difficulty=beginner&src=topic`}
|
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"
|
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}
|
{subject}
|
||||||
</a>
|
</a>
|
||||||
@@ -349,7 +268,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
|||||||
<button
|
<button
|
||||||
className="rounded-md bg-white px-2 py-2 text-xs font-medium text-black hover:bg-gray-200"
|
className="rounded-md bg-white px-2 py-2 text-xs font-medium text-black hover:bg-gray-200"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAiChatHistory(defaultChatHistory);
|
setMessages([]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-3.5" />
|
<Trash2 className="size-3.5" />
|
||||||
@@ -416,39 +335,9 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className="scrollbar-thumb-gray-300 scrollbar-track-transparent scrollbar-thin relative grow overflow-y-auto"
|
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">
|
<TopicChatMessages messages={messages} status={status} />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
@@ -517,7 +406,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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"
|
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]" />
|
<SendIcon className="size-4 stroke-[2.5]" />
|
||||||
|
85
src/hooks/use-ai-chat-scroll.tsx
Normal file
85
src/hooks/use-ai-chat-scroll.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import type { UIMessage } from 'ai';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
type UseAIChatScrollProps = {
|
||||||
|
messages: UIMessage[];
|
||||||
|
threshold?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useAIChatScroll(props: UseAIChatScrollProps) {
|
||||||
|
const { messages, threshold = 80 } = props;
|
||||||
|
|
||||||
|
const scrollableContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [showScrollToBottomButton, setShowScrollToBottomButton] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const canScrollToBottom = useCallback(() => {
|
||||||
|
const scrollableContainer = scrollableContainerRef?.current;
|
||||||
|
if (!scrollableContainer) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paddingBottom = parseInt(
|
||||||
|
getComputedStyle(scrollableContainer).paddingBottom
|
||||||
|
);
|
||||||
|
|
||||||
|
const distanceFromBottom =
|
||||||
|
scrollableContainer.scrollHeight -
|
||||||
|
(scrollableContainer.scrollTop + scrollableContainer.clientHeight) -
|
||||||
|
paddingBottom;
|
||||||
|
|
||||||
|
return distanceFromBottom > -(paddingBottom - threshold);
|
||||||
|
}, [threshold]);
|
||||||
|
|
||||||
|
const scrollToBottom = useCallback(
|
||||||
|
(behavior: 'instant' | 'smooth' = 'smooth') => {
|
||||||
|
const scrollableContainer = scrollableContainerRef?.current;
|
||||||
|
if (!scrollableContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollableContainer.scrollTo({
|
||||||
|
top: scrollableContainer.scrollHeight,
|
||||||
|
behavior: behavior === 'instant' ? 'instant' : 'smooth',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[scrollableContainerRef]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const scrollableContainer = scrollableContainerRef.current;
|
||||||
|
if (!scrollableContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const abortController = new AbortController();
|
||||||
|
let timeoutId: NodeJS.Timeout;
|
||||||
|
const debouncedHandleScroll = () => {
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
setShowScrollToBottomButton(canScrollToBottom());
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
debouncedHandleScroll();
|
||||||
|
scrollableContainer.addEventListener('scroll', debouncedHandleScroll, {
|
||||||
|
signal: abortController.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
abortController.abort();
|
||||||
|
};
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
scrollableContainerRef,
|
||||||
|
showScrollToBottomButton,
|
||||||
|
scrollToBottom,
|
||||||
|
};
|
||||||
|
}
|
119
src/hooks/use-completion.ts
Normal file
119
src/hooks/use-completion.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { useCallback, useRef, useState } from 'react';
|
||||||
|
import { fetchWithAuthHandling } from '../lib/ai';
|
||||||
|
import type { CompletionPart } from '../lib/stream';
|
||||||
|
import { readDataStream } from '../lib/stream';
|
||||||
|
|
||||||
|
type CompleteOptions = {
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
body?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CompletionStatus = 'idle' | 'loading' | 'streaming' | 'success' | 'error';
|
||||||
|
|
||||||
|
export type CompletionContext = {
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CompletionParams<D extends Record<string, unknown>> = {
|
||||||
|
endpoint: string;
|
||||||
|
onStart?: (options?: CompleteOptions) => Promise<void> | void;
|
||||||
|
onData?: (
|
||||||
|
part: CompletionPart<D>,
|
||||||
|
context: CompletionContext
|
||||||
|
) => Promise<void> | void;
|
||||||
|
onFinish?: (
|
||||||
|
result: string,
|
||||||
|
context: CompletionContext
|
||||||
|
) => Promise<void> | void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useCompletion<
|
||||||
|
D extends Record<string, unknown> = Record<string, unknown>,
|
||||||
|
>(params: CompletionParams<D>) {
|
||||||
|
const { endpoint, onData, onFinish, onError, onStart } = params;
|
||||||
|
|
||||||
|
const [status, setStatus] = useState<CompletionStatus>('idle');
|
||||||
|
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
const [completion, setCompletion] = useState<string>('');
|
||||||
|
|
||||||
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
|
const complete = useCallback(
|
||||||
|
async (options?: CompleteOptions) => {
|
||||||
|
setStatus('loading');
|
||||||
|
setError(null);
|
||||||
|
setCompletion('');
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
abortControllerRef.current = controller;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await onStart?.(options);
|
||||||
|
|
||||||
|
const url = endpoint.startsWith('http')
|
||||||
|
? endpoint
|
||||||
|
: `${import.meta.env.VITE_API_URL}${endpoint}`;
|
||||||
|
const response = await fetchWithAuthHandling(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options?.headers,
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
...(options?.body ? { body: JSON.stringify(options.body) } : {}),
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const stream = response.body;
|
||||||
|
if (!stream) {
|
||||||
|
throw new Error('No stream found');
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus('streaming');
|
||||||
|
let result = '';
|
||||||
|
await readDataStream<D>(stream, {
|
||||||
|
onData: async (part) => {
|
||||||
|
if (part.type === 'text') {
|
||||||
|
result += part.content;
|
||||||
|
setCompletion(result);
|
||||||
|
}
|
||||||
|
await onData?.(part, { content: result });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await onFinish?.(result, { content: result });
|
||||||
|
|
||||||
|
abortControllerRef.current = null;
|
||||||
|
setStatus('success');
|
||||||
|
setCompletion(result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
// we can ignore abort errors
|
||||||
|
// as they are expected when the user cancels the request
|
||||||
|
if (error instanceof Error && error.name === 'AbortError') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(error as Error);
|
||||||
|
onError?.(error as Error);
|
||||||
|
setStatus('error');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[endpoint, onData, onFinish, onError, onStart]
|
||||||
|
);
|
||||||
|
|
||||||
|
const stop = useCallback(() => {
|
||||||
|
if (!abortControllerRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
abortControllerRef.current = null;
|
||||||
|
}, [abortControllerRef]);
|
||||||
|
|
||||||
|
return { status, error, stop, complete, completion, setCompletion };
|
||||||
|
}
|
23
src/hooks/use-is-thinking.ts
Normal file
23
src/hooks/use-is-thinking.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import type { ChatStatus, UIMessage } from 'ai';
|
||||||
|
|
||||||
|
export function useIsThinking(messages: UIMessage[], status: ChatStatus) {
|
||||||
|
return useMemo(() => {
|
||||||
|
const lastMessage = messages.at(-1);
|
||||||
|
if (!lastMessage) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'submitted' && lastMessage.role === 'user') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasText =
|
||||||
|
lastMessage.role === 'assistant' &&
|
||||||
|
lastMessage.parts.some(
|
||||||
|
(part) => part.type === 'text' && part.text?.trim(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (status === 'submitted' || status === 'streaming') && !hasText;
|
||||||
|
}, [messages, status]);
|
||||||
|
}
|
@@ -1,4 +1,8 @@
|
|||||||
|
import Cookies from 'js-cookie';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
import { TOKEN_COOKIE_NAME } from './jwt';
|
||||||
|
import { FetchError } from './query-http';
|
||||||
|
import { DefaultChatTransport, type UIMessage } from 'ai';
|
||||||
|
|
||||||
export const IS_KEY_ONLY_ROADMAP_GENERATION = false;
|
export const IS_KEY_ONLY_ROADMAP_GENERATION = false;
|
||||||
|
|
||||||
@@ -353,3 +357,56 @@ export function generateAICourseRoadmapStructure(
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ChatUIMessage = UIMessage<
|
||||||
|
never,
|
||||||
|
{
|
||||||
|
redirect: {
|
||||||
|
title: string;
|
||||||
|
chatId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const chatRoadmapTransport = new DefaultChatTransport({
|
||||||
|
api: import.meta.env.PUBLIC_API_URL + '/v1-chat-roadmap',
|
||||||
|
credentials: 'include',
|
||||||
|
fetch: fetchWithAuthHandling,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const topicDetailAiChatTransport = new DefaultChatTransport({
|
||||||
|
api: import.meta.env.PUBLIC_API_URL + '/v1-topic-detail-chat',
|
||||||
|
credentials: 'include',
|
||||||
|
fetch: fetchWithAuthHandling,
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function fetchWithAuthHandling(
|
||||||
|
input: RequestInfo | URL,
|
||||||
|
init?: RequestInit,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(input, init);
|
||||||
|
if (response.status === 401) {
|
||||||
|
Cookies.remove(TOKEN_COOKIE_NAME);
|
||||||
|
window?.location?.reload();
|
||||||
|
return null as unknown as Response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data?.errors) {
|
||||||
|
throw new FetchError(response.status, data.message);
|
||||||
|
} else {
|
||||||
|
throw new Error('An unexpected error occurred');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (typeof navigator !== 'undefined' && !navigator.onLine) {
|
||||||
|
throw new FetchError(503, 'Service Unavailable');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
133
src/lib/message-part.ts
Normal file
133
src/lib/message-part.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
|
type MessagePart = {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
text?: string;
|
||||||
|
data?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MessagePartRendererProps = {
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MessagePartRenderer = (props: MessagePartRendererProps) => any;
|
||||||
|
|
||||||
|
export function parseMessageParts(
|
||||||
|
content: string,
|
||||||
|
renderer: Record<string, MessagePartRenderer>,
|
||||||
|
) {
|
||||||
|
const parts: MessagePart[] = [];
|
||||||
|
const tagNames = Object.keys(renderer);
|
||||||
|
|
||||||
|
// Remove codeblocks around custom tags
|
||||||
|
if (tagNames.length > 0) {
|
||||||
|
const tagPattern = tagNames.join('|');
|
||||||
|
|
||||||
|
// Remove opening codeblock before tags: ```lang\n<tag> -> <tag>
|
||||||
|
// It sometimes puts codeblocks around our tags (despite us asking it not to)
|
||||||
|
// so we manually remove them here
|
||||||
|
content = content.replace(
|
||||||
|
new RegExp(`\`\`\`\\w*?\\n+?<(${tagPattern})>`, 'g'),
|
||||||
|
'<$1>',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove closing codeblock after tags: </tag>\n``` -> </tag>
|
||||||
|
content = content.replace(
|
||||||
|
new RegExp(`<\\/(${tagPattern})>\\n+?\`\`\``, 'g'),
|
||||||
|
'</$1>',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no renderers, just return the content as markdown
|
||||||
|
if (tagNames.length === 0) {
|
||||||
|
parts.push({
|
||||||
|
id: nanoid(),
|
||||||
|
type: 'text',
|
||||||
|
text: content,
|
||||||
|
});
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagPattern = tagNames.join('|');
|
||||||
|
const regex = new RegExp(`<(${tagPattern})>(.*?)<\/\\1>`, 'gs');
|
||||||
|
|
||||||
|
let lastIndex = 0;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
// we will match only tags that have renderers
|
||||||
|
// and then we will render each tag with the corresponding renderer
|
||||||
|
// and then we will push the rendered content to the parts array
|
||||||
|
while ((match = regex.exec(content)) !== null) {
|
||||||
|
const [_, tag, innerContent] = match;
|
||||||
|
|
||||||
|
// push the text before the tag
|
||||||
|
// so that we can render it later
|
||||||
|
if (match.index > lastIndex) {
|
||||||
|
const rawBefore = content.slice(lastIndex, match.index);
|
||||||
|
parts.push({
|
||||||
|
id: nanoid(),
|
||||||
|
type: 'text',
|
||||||
|
text: rawBefore,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = renderer[tag]({
|
||||||
|
content: innerContent,
|
||||||
|
});
|
||||||
|
parts.push({
|
||||||
|
id: nanoid(),
|
||||||
|
type: tag,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
// update the last index
|
||||||
|
// so that we can render the next tag
|
||||||
|
lastIndex = regex.lastIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there was an opening tag that never closed, check manually
|
||||||
|
// search for any known tag that starts but wasn't matched
|
||||||
|
for (const tag of tagNames) {
|
||||||
|
const openingTag = `<${tag}>`;
|
||||||
|
const openingIndex = content.indexOf(openingTag, lastIndex);
|
||||||
|
const closingTag = `</${tag}>`;
|
||||||
|
const closingIndex = content.indexOf(closingTag, lastIndex);
|
||||||
|
|
||||||
|
if (openingIndex !== -1 && closingIndex === -1) {
|
||||||
|
if (openingIndex > lastIndex) {
|
||||||
|
const rawBefore = content.slice(lastIndex, openingIndex);
|
||||||
|
parts.push({
|
||||||
|
id: nanoid(),
|
||||||
|
type: 'text',
|
||||||
|
text: rawBefore,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const innerContent = content.slice(openingIndex + openingTag.length);
|
||||||
|
const data = renderer[tag]({
|
||||||
|
content: innerContent,
|
||||||
|
});
|
||||||
|
parts.push({
|
||||||
|
id: nanoid(),
|
||||||
|
type: tag,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the remaining content
|
||||||
|
if (lastIndex < content.length) {
|
||||||
|
const rawRemaining = content.slice(lastIndex);
|
||||||
|
|
||||||
|
parts.push({
|
||||||
|
id: nanoid(),
|
||||||
|
type: 'text',
|
||||||
|
text: rawRemaining,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
}
|
100
src/lib/stream.ts
Normal file
100
src/lib/stream.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
export const CHAT_RESPONSE_PREFIX = {
|
||||||
|
message: '0',
|
||||||
|
details: 'd',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const NEWLINE = '\n'.charCodeAt(0);
|
||||||
|
|
||||||
|
function concatChunks(chunks: Uint8Array[], totalLength: number) {
|
||||||
|
const concatenatedChunks = new Uint8Array(totalLength);
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
concatenatedChunks.set(chunk, offset);
|
||||||
|
offset += chunk.length;
|
||||||
|
}
|
||||||
|
chunks.length = 0;
|
||||||
|
|
||||||
|
return concatenatedChunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CompletionTextPart = {
|
||||||
|
type: 'text';
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CompletionDetailsPart<D extends Record<string, unknown>> = {
|
||||||
|
type: 'details';
|
||||||
|
data: D;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CompletionPart<
|
||||||
|
D extends Record<string, unknown> = Record<string, unknown>,
|
||||||
|
> = CompletionTextPart | CompletionDetailsPart<D>;
|
||||||
|
|
||||||
|
export async function readDataStream<D extends Record<string, unknown>>(
|
||||||
|
stream: ReadableStream<Uint8Array>,
|
||||||
|
{
|
||||||
|
onData,
|
||||||
|
onFinish,
|
||||||
|
}: {
|
||||||
|
onData?: (part: CompletionPart<D>) => Promise<void> | void;
|
||||||
|
onFinish?: () => Promise<void> | void;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const reader = stream.getReader();
|
||||||
|
const decoder = new TextDecoder('utf-8');
|
||||||
|
const chunks: Uint8Array[] = [];
|
||||||
|
|
||||||
|
let totalLength = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { value } = await reader.read();
|
||||||
|
if (value) {
|
||||||
|
chunks.push(value);
|
||||||
|
totalLength += value.length;
|
||||||
|
if (value[value.length - 1] !== NEWLINE) {
|
||||||
|
// if the last character is not a new line, we need to wait for the next chunk
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunks.length === 0) {
|
||||||
|
// end of stream
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const concatenatedChunks = concatChunks(chunks, totalLength);
|
||||||
|
totalLength = 0;
|
||||||
|
|
||||||
|
const streamParts: CompletionPart<D>[] = decoder
|
||||||
|
.decode(concatenatedChunks, { stream: true })
|
||||||
|
.split('\n')
|
||||||
|
.filter((line) => line !== '')
|
||||||
|
.map((line) => {
|
||||||
|
const separatorIndex = line.indexOf(':');
|
||||||
|
if (separatorIndex === -1) {
|
||||||
|
throw new Error('Invalid line: ' + line + '. No separator found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = line.slice(0, separatorIndex);
|
||||||
|
const content = line.slice(separatorIndex + 1);
|
||||||
|
|
||||||
|
switch (prefix) {
|
||||||
|
case CHAT_RESPONSE_PREFIX.message:
|
||||||
|
return { type: 'text', content: JSON.parse(content) };
|
||||||
|
case CHAT_RESPONSE_PREFIX.details:
|
||||||
|
return { type: 'details', data: JSON.parse(content) };
|
||||||
|
default:
|
||||||
|
throw new Error('Invalid prefix: ' + prefix);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const part of streamParts) {
|
||||||
|
await onData?.(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await onFinish?.();
|
||||||
|
reader.releaseLock();
|
||||||
|
}
|
@@ -12,6 +12,8 @@ import {
|
|||||||
type RoadmapAIChatHistoryType,
|
type RoadmapAIChatHistoryType,
|
||||||
} from '../hooks/use-roadmap-ai-chat';
|
} from '../hooks/use-roadmap-ai-chat';
|
||||||
import type { JSONContent } from '@tiptap/core';
|
import type { JSONContent } from '@tiptap/core';
|
||||||
|
import type { UIMessage } from 'ai';
|
||||||
|
import type { ChatUIMessage } from '../lib/ai';
|
||||||
|
|
||||||
export type ChatHistoryMessage = {
|
export type ChatHistoryMessage = {
|
||||||
_id: string;
|
_id: string;
|
||||||
@@ -26,16 +28,13 @@ export interface ChatHistoryDocument {
|
|||||||
userId: string;
|
userId: string;
|
||||||
roadmapId?: string;
|
roadmapId?: string;
|
||||||
title: string;
|
title: string;
|
||||||
messages: ChatHistoryMessage[];
|
messages: ChatUIMessage[];
|
||||||
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function chatHistoryOptions(
|
export function chatHistoryOptions(chatHistoryId?: string) {
|
||||||
chatHistoryId?: string,
|
|
||||||
renderer?: Record<string, MessagePartRenderer>,
|
|
||||||
) {
|
|
||||||
return queryOptions({
|
return queryOptions({
|
||||||
queryKey: ['chat-history-details', chatHistoryId],
|
queryKey: ['chat-history-details', chatHistoryId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -47,31 +46,7 @@ export function chatHistoryOptions(
|
|||||||
document.title = data.title;
|
document.title = data.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages: RoadmapAIChatHistoryType[] = [];
|
return data;
|
||||||
for (const message of data.messages) {
|
|
||||||
messages.push({
|
|
||||||
role: message.role,
|
|
||||||
content: message.content,
|
|
||||||
...(message.role === 'user' &&
|
|
||||||
!message?.json && {
|
|
||||||
html: markdownToHtml(message.content),
|
|
||||||
}),
|
|
||||||
...(message.role === 'user' &&
|
|
||||||
message?.json && {
|
|
||||||
html: htmlFromTiptapJSON(message.json),
|
|
||||||
}),
|
|
||||||
...(message.role === 'assistant' && {
|
|
||||||
jsx: await renderMessage(message.content, renderer ?? {}, {
|
|
||||||
isLoading: false,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...data,
|
|
||||||
messages,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
enabled: !!isLoggedIn() && !!chatHistoryId,
|
enabled: !!isLoggedIn() && !!chatHistoryId,
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user