Compare commits

..

12 Commits

Author SHA1 Message Date
Samy Pessé 75e34fa299 Continue 2025-06-09 00:46:23 +02:00
Samy Pessé c8a2fd8351 Use stream of document for summary 2025-06-05 11:34:20 +01:00
Samy Pessé 9466913e18 Merge branch 'main' into new-search-ui 2025-06-04 16:36:08 +01:00
Samy Pessé ef57c608a5 Merge branch 'main' into new-search-ui 2025-06-04 16:04:08 +01:00
Zeno Kapitein d678a9162d Fix typing & translations 2025-05-23 16:54:28 +02:00
Zeno Kapitein af7cadc8e9 Added back and forth chat 2025-05-21 18:08:02 +02:00
Zeno Kapitein e2b2468475 More layout tweaks 2025-05-21 15:13:22 +02:00
Zeno Kapitein 11f4241709 Update layout and transitions 2025-05-21 14:02:48 +02:00
Zeno Kapitein 5a410feda0 Add chat component (non-functional) 2025-05-21 14:02:48 +02:00
Zeno Kapitein 31bfe77f74 Create Chat component 2025-05-21 14:02:48 +02:00
Zeno Kapitein 34dceb2f97 Toggleable layout 2025-05-21 14:02:48 +02:00
Zeno Kapitein cced4f0d78 Initial styling 2025-05-21 14:02:48 +02:00
158 changed files with 2188 additions and 2885 deletions
-9
View File
@@ -1,9 +0,0 @@
---
"gitbook": patch
---
Fix three small visual issues
- Fix sidebar showing on `no-toc` pages in the gradient theme
- Fix variant selector truncating incorrectly in header when sections are present
- Fix page cover alignment on `lg` screens without TOC
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook-v2": patch
---
Add docs.testgitbook.com to ADAPTIVE_CONTENT_HOSTS list
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": minor
---
Add circular corners and depth styling
-6
View File
@@ -1,6 +0,0 @@
---
'@gitbook/react-openapi': patch
'gitbook': patch
---
Improve support for OAuth2 security type
-6
View File
@@ -1,6 +0,0 @@
---
"gitbook-v2": patch
"gitbook": patch
---
Adds Columns layout block to GBO
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": minor
---
Support dark-mode specific page cover image
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': patch
---
Update Models page styling
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": minor
---
Best effort at preserving current variant when navigating between sections by matching the pathname against site spaces in the new section.
-6
View File
@@ -1,6 +0,0 @@
---
"gitbook-v2": patch
"gitbook": patch
---
encode customization header
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": minor
---
Rework full-width layout, add support for full-width page option
-6
View File
@@ -1,6 +0,0 @@
---
"gitbook-v2": patch
"gitbook": patch
---
Fix concurrent execution in Vercel causing pages to not be attached to the proper tags.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook-v2": patch
---
Generate a llms-full.txt version of the docs site
-5
View File
@@ -1,5 +0,0 @@
---
"@gitbook/fonts": minor
---
Initial version of the package
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Make icons for page groups more contrasting
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix border being added to cards
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook-v2": patch
---
apply customization for dynamic context
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Reverse order of feedback smileys
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix ogimage using incorrect Google Font depending on language.
+4 -4
View File
@@ -159,7 +159,7 @@ jobs:
runs-on: ubuntu-latest
name: Visual Testing v1
needs: deploy-v1-cloudflare
timeout-minutes: 10
timeout-minutes: 8
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -220,7 +220,7 @@ jobs:
runs-on: ubuntu-latest
name: Visual Testing Customers v1
needs: deploy-v1-cloudflare
timeout-minutes: 10
timeout-minutes: 8
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -240,7 +240,7 @@ jobs:
runs-on: ubuntu-latest
name: Visual Testing Customers v2
needs: deploy-v2-vercel
timeout-minutes: 10
timeout-minutes: 8
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -261,7 +261,7 @@ jobs:
runs-on: ubuntu-latest
name: Visual Testing Customers v2 (Cloudflare)
needs: deploy-v2-cloudflare
timeout-minutes: 10
timeout-minutes: 8
steps:
- name: Checkout
uses: actions/checkout@v4
+16 -133
View File
@@ -26,7 +26,7 @@
"name": "@gitbook/cache-tags",
"version": "0.3.1",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/api": "^0.117.1",
"assert-never": "^1.2.1",
},
"devDependencies": {
@@ -47,27 +47,15 @@
"emoji-assets": "^8.0.0",
},
},
"packages/fonts": {
"name": "@gitbook/fonts",
"version": "0.0.0",
"dependencies": {
"@gitbook/api": "catalog:",
},
"devDependencies": {
"google-font-metadata": "^6.0.3",
"typescript": "^5.5.3",
},
},
"packages/gitbook": {
"name": "gitbook",
"version": "0.12.0",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/api": "^0.117.1",
"@gitbook/cache-do": "workspace:*",
"@gitbook/cache-tags": "workspace:*",
"@gitbook/colors": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*",
"@gitbook/fonts": "workspace:*",
"@gitbook/icons": "workspace:*",
"@gitbook/openapi-parser": "workspace:*",
"@gitbook/react-contentkit": "workspace:*",
@@ -92,19 +80,14 @@
"jwt-decode": "^4.0.0",
"katex": "^0.16.9",
"mathjax": "^3.2.2",
"mdast-util-from-markdown": "^2.0.2",
"mdast-util-frontmatter": "^2.0.1",
"mdast-util-gfm": "^3.1.0",
"mdast-util-to-markdown": "^2.1.2",
"memoizee": "^0.4.17",
"micromark-extension-frontmatter": "^2.0.0",
"micromark-extension-gfm": "^3.0.0",
"next": "14.2.26",
"next-themes": "^0.2.1",
"nuqs": "^2.2.3",
"object-hash": "^3.0.0",
"openapi-types": "^12.1.3",
"p-map": "^7.0.3",
"p-map": "^7.0.0",
"parse-cache-control": "^1.0.1",
"partial-json": "^0.1.7",
"react": "^19.0.0",
@@ -121,8 +104,6 @@
"tailwind-merge": "^2.2.0",
"tailwind-shades": "^1.1.2",
"unified": "^11.0.5",
"unist-util-remove": "^4.0.0",
"unist-util-visit": "^5.0.0",
"url-join": "^5.0.0",
"usehooks-ts": "^3.1.0",
"zod": "^3.24.2",
@@ -162,9 +143,9 @@
"name": "gitbook-v2",
"version": "0.3.0",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/api": "^0.117.1",
"@gitbook/cache-tags": "workspace:*",
"@opennextjs/cloudflare": "1.2.1",
"@opennextjs/cloudflare": "1.1.0",
"@sindresorhus/fnv1a": "^3.1.0",
"assert-never": "^1.2.1",
"jwt-decode": "^4.0.0",
@@ -221,7 +202,7 @@
"name": "@gitbook/react-contentkit",
"version": "0.7.0",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/api": "^0.117.1",
"@gitbook/icons": "workspace:*",
"classnames": "^2.5.1",
},
@@ -279,12 +260,10 @@
},
"overrides": {
"@codemirror/state": "6.4.1",
"@gitbook/api": "^0.117.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
},
"catalog": {
"@gitbook/api": "^0.120.0",
},
"packages": {
"@ai-sdk/provider": ["@ai-sdk/provider@1.1.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="],
@@ -618,8 +597,6 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg=="],
"@evan/concurrency": ["@evan/concurrency@0.0.3", "", {}, "sha512-vjhkm2nrXoM39G4aP/U4CC5vFv/ZlMRSjbTII0N65J9R0EpgjdGswnHKS1KSjfGAp/9zKSNaojBgi0SxKnGapw=="],
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
"@floating-ui/core": ["@floating-ui/core@1.6.8", "", { "dependencies": { "@floating-ui/utils": "^0.2.8" } }, "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA=="],
@@ -648,7 +625,7 @@
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="],
"@gitbook/api": ["@gitbook/api@0.120.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-FiRmPiSBwobMxmNjd14QkkOdM95BAPLDDRShgpS9Vsd8lHjNMyZfrJKVJTsJUuFcgYoi4cqNw9yu/TiUBUgv3g=="],
"@gitbook/api": ["@gitbook/api@0.117.1", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-edyi4Y+Zs/9vdU6CvJfwtVSiEWrKzzlp+sIfsyw8WT+mJWnH9CN+o17nxvRWNmAznU+YZm+Qz7itVbG8k+vSkQ=="],
"@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"],
@@ -660,8 +637,6 @@
"@gitbook/fontawesome-pro": ["@gitbook/fontawesome-pro@1.0.8", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "^6.6.0" } }, "sha512-i4PgiuGyUb52Muhc52kK3aMJIMfMkA2RbPW30tre8a6M8T6mWTfYo6gafSgjNvF1vH29zcuB8oBYnF0gO4XcHA=="],
"@gitbook/fonts": ["@gitbook/fonts@workspace:packages/fonts"],
"@gitbook/icons": ["@gitbook/icons@workspace:packages/icons"],
"@gitbook/openapi-parser": ["@gitbook/openapi-parser@workspace:packages/openapi-parser"],
@@ -816,25 +791,9 @@
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@octokit/auth-token": ["@octokit/auth-token@5.1.2", "", {}, "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw=="],
"@opennextjs/aws": ["@opennextjs/aws@3.6.4", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-/bn9N/6dVu9+sC7AptaGJylKUzyDDgqe3yKfvUxXOpy7whIq/+3Bw+q2/bilyQg6FcskbCWUt9nLvNf1hAVmfA=="],
"@octokit/core": ["@octokit/core@6.1.5", "", { "dependencies": { "@octokit/auth-token": "^5.0.0", "@octokit/graphql": "^8.2.2", "@octokit/request": "^9.2.3", "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0", "before-after-hook": "^3.0.2", "universal-user-agent": "^7.0.0" } }, "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg=="],
"@octokit/endpoint": ["@octokit/endpoint@10.1.4", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA=="],
"@octokit/graphql": ["@octokit/graphql@8.2.2", "", { "dependencies": { "@octokit/request": "^9.2.3", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA=="],
"@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
"@octokit/request": ["@octokit/request@9.2.3", "", { "dependencies": { "@octokit/endpoint": "^10.1.4", "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^2.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w=="],
"@octokit/request-error": ["@octokit/request-error@6.1.8", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ=="],
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
"@opennextjs/aws": ["@opennextjs/aws@3.6.5", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "cookie": "^1.0.2", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-wni+CWlRCyWfhNfekQBBPPkrDDnaGdZLN9hMybKI0wKOKTO+zhPOqR65Eh3V0pzWAi84Sureb5mdMuLwCxAAcw=="],
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.2.1", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.6.5", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.19.1" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-cOco+nHwlo/PLB1bThF8IIvaS8PgAT9MEI5ZttFO/qt6spgvr2lUaPkpjgSIQmI3sBIEG2cLUykvQ2nbbZEcVw=="],
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.1.0", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.6.4", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.14.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-8DOzpLiewivA1pwRNGVPDXbrF79bzJiHscY+M1tekwvKqEqM26CRYafhrA5IDCeyteNaL9AZD6X4DLuCn/eeYQ=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
@@ -1504,8 +1463,6 @@
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"before-after-hook": ["before-after-hook@3.0.2", "", {}, "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A=="],
"better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="],
"bignumber.js": ["bignumber.js@9.1.2", "", {}, "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug=="],
@@ -1518,8 +1475,6 @@
"body-parser": ["body-parser@2.0.2", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "3.1.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.5.2", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "^3.0.0", "type-is": "~1.6.18" } }, "sha512-SNMk0OONlQ01uk8EPeiBvTW7W4ovpL5b1O3t1sjpPgfxOQ6BqQJ6XjxinDPR79Z6HdcD5zBBwr5ssiTlgdNztQ=="],
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
"bowser": ["bowser@2.11.0", "", {}, "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="],
"boxen": ["boxen@4.2.0", "", { "dependencies": { "ansi-align": "^3.0.0", "camelcase": "^5.3.1", "chalk": "^3.0.0", "cli-boxes": "^2.2.0", "string-width": "^4.1.0", "term-size": "^2.1.0", "type-fest": "^0.8.1", "widest-line": "^3.1.0" } }, "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ=="],
@@ -1544,8 +1499,6 @@
"bytes": ["bytes@3.1.0", "", {}, "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="],
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"cacheable": ["cacheable@1.8.9", "", { "dependencies": { "hookified": "^1.7.1", "keyv": "^5.3.1" } }, "sha512-FicwAUyWnrtnd4QqYAoRlNs44/a1jTL7XDKqm5gJ90wz1DQPlC7U2Rd1Tydpv+E7WAr4sQHuw8Q8M3nZMAyecQ=="],
"cacheable-request": ["cacheable-request@6.1.0", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^3.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^4.1.0", "responselike": "^1.0.2" } }, "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg=="],
@@ -1642,16 +1595,10 @@
"css-functions-list": ["css-functions-list@3.2.3", "", {}, "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA=="],
"css-select": ["css-select@5.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg=="],
"css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
"css-what": ["css-what@6.1.0", "", {}, "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"cssom": ["cssom@0.5.0", "", {}, "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"cva": ["cva@1.0.0-beta.2", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "typescript": ">= 4.5.5 < 6" }, "optionalPeers": ["typescript"] }, "sha512-dqcOFe247I5pKxfuzqfq3seLL5iMYsTgo40Uw7+pKZAntPgFtR7Tmy59P5IVIq/XgB0NQWoIvYDt9TwHkuK8Cg=="],
@@ -1708,14 +1655,6 @@
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
"dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="],
"dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="],
@@ -1748,7 +1687,7 @@
"enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="],
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"env-cmd": ["env-cmd@10.1.0", "", { "dependencies": { "commander": "^4.0.0", "cross-spawn": "^7.0.0" }, "bin": { "env-cmd": "bin/env-cmd.js" } }, "sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA=="],
@@ -1854,8 +1793,6 @@
"external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="],
"fast-content-type-parse": ["fast-content-type-parse@2.0.1", "", {}, "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
@@ -1872,8 +1809,6 @@
"fastq": ["fastq@1.17.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w=="],
"fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="],
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
"fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="],
@@ -1900,8 +1835,6 @@
"form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="],
"format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="],
@@ -1960,8 +1893,6 @@
"google-auth-library": ["google-auth-library@5.10.1", "", { "dependencies": { "arrify": "^2.0.0", "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", "gaxios": "^2.1.0", "gcp-metadata": "^3.4.0", "gtoken": "^4.1.0", "jws": "^4.0.0", "lru-cache": "^5.0.0" } }, "sha512-rOlaok5vlpV9rSiUu5EpR0vVpc+PhN62oF4RyX/6++DG1VsaulAFEMlDYBLjJDDPI6OcNOCGAKy9UVB/3NIDXg=="],
"google-font-metadata": ["google-font-metadata@6.0.3", "", { "dependencies": { "@evan/concurrency": "^0.0.3", "@octokit/core": "^6.1.2", "cac": "^6.7.14", "consola": "^3.3.3", "deepmerge": "^4.3.1", "json-stringify-pretty-compact": "^4.0.0", "linkedom": "^0.18.6", "pathe": "^1.1.2", "picocolors": "^1.1.1", "playwright": "^1.49.1", "stylis": "^4.3.4", "zod": "^3.24.1" }, "bin": { "gfm": "dist/cli.mjs" } }, "sha512-62+QB+nmBKppHrxwvILZkV/EvKctEAdzkKBIJY26TtwZkUjeEqmAQnE5uHvtMTdB8odqGpQu5LAKPqqUYHI9wA=="],
"google-p12-pem": ["google-p12-pem@2.0.5", "", { "dependencies": { "node-forge": "^0.10.0" }, "bin": { "gp12-pem": "build/src/bin/gp12-pem.js" } }, "sha512-7RLkxwSsMsYh9wQ5Vb2zRtkAHvqPvfoMGag+nugl1noYO7gf0844Yr9TIFA5NEBMAeVt2Z+Imu7CQMp3oNatzQ=="],
"googleapis": ["googleapis@47.0.0", "", { "dependencies": { "google-auth-library": "^5.6.1", "googleapis-common": "^3.2.0" } }, "sha512-+Fnjgcc3Na/rk57dwxqW1V0HJXJFjnt3aqFlckULqAqsPkmex/AyJJe6MSlXHC37ZmlXEb9ZtPGUp5ZzRDXpHg=="],
@@ -2038,16 +1969,12 @@
"hosted-git-info": ["hosted-git-info@2.8.9", "", {}, "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="],
"html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="],
"html-tags": ["html-tags@3.3.1", "", {}, "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ=="],
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
"html-whitespace-sensitive-tag-names": ["html-whitespace-sensitive-tag-names@3.0.1", "", {}, "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA=="],
"htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="],
"http-cache-semantics": ["http-cache-semantics@4.1.1", "", {}, "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="],
"http-errors": ["http-errors@1.4.0", "", { "dependencies": { "inherits": "2.0.1", "statuses": ">= 1.2.1 < 2" } }, "sha512-oLjPqve1tuOl5aRhv8GK5eHpqP1C9fb+Ol+XTLjKfLltE44zdDbEdjPSbU7Ch5rSNsVFqZn97SrMmZLdu1/YMw=="],
@@ -2158,8 +2085,6 @@
"json-stringify-deterministic": ["json-stringify-deterministic@1.0.12", "", {}, "sha512-q3PN0lbUdv0pmurkBNdJH3pfFvOTL/Zp0lquqpvcjfKzt6Y0j49EPHAmVHCAS4Ceq/Y+PejWTzyiVpoY71+D6g=="],
"json-stringify-pretty-compact": ["json-stringify-pretty-compact@4.0.0", "", {}, "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q=="],
"json-xml-parse": ["json-xml-parse@1.3.0", "", {}, "sha512-MVosauc/3W2wL4dd4yaJzH5oXw+HOUfptn0+d4+bFghMiJFop7MaqIwFXJNLiRnNYJNQ6L4o7B+53n5wcvoLFw=="],
"jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="],
@@ -2198,8 +2123,6 @@
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"linkedom": ["linkedom@0.18.11", "", { "dependencies": { "css-select": "^5.1.0", "cssom": "^0.5.0", "html-escaper": "^3.0.3", "htmlparser2": "^10.0.0", "uhyphen": "^0.2.0" } }, "sha512-K03GU3FUlnhBAP0jPb7tN7YJl7LbjZx30Z8h6wgLXusnKF7+BEZvfEbdkN/lO9LfFzxN3S0ZAriDuJ/13dIsLA=="],
"locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
"lodash.castarray": ["lodash.castarray@4.4.0", "", {}, "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q=="],
@@ -2258,11 +2181,9 @@
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA=="],
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="],
"mdast-util-frontmatter": ["mdast-util-frontmatter@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "escape-string-regexp": "^5.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0" } }, "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA=="],
"mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
"mdast-util-gfm": ["mdast-util-gfm@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw=="],
"mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
@@ -2306,8 +2227,6 @@
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA=="],
"micromark-extension-frontmatter": ["micromark-extension-frontmatter@2.0.0", "", { "dependencies": { "fault": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg=="],
"micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
"micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
@@ -2430,8 +2349,6 @@
"npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
"nuqs": ["nuqs@2.2.3", "", { "dependencies": { "mitt": "^3.0.1" }, "peerDependencies": { "@remix-run/react": ">=2", "next": ">=14.2.0", "react": ">=18.2.0 || ^19.0.0-0", "react-router-dom": ">=6" }, "optionalPeers": ["@remix-run/react", "next", "react-router-dom"] }, "sha512-nMCcUW06KSqEXA0xp+LiRqDpIE59BVYbjZLe0HUisJAlswfihHYSsAjYTzV0lcE1thfh8uh+LqUHGdQ8qq8rfA=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
@@ -2478,7 +2395,7 @@
"p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
"p-map": ["p-map@7.0.3", "", {}, "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA=="],
"p-map": ["p-map@7.0.2", "", {}, "sha512-z4cYYMMdKHzw4O5UkWJImbZynVIo0lSGTXc7bzB1e/rrDqkgGUNysK/o4bTr+0+xKvvLoTyGqYC4Fgljy9qe1Q=="],
"p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
@@ -2522,7 +2439,7 @@
"path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
"pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"pcre-to-regexp": ["pcre-to-regexp@1.1.0", "", {}, "sha512-KF9XxmUQJ2DIlMj3TqNqY1AWvyvTuIuq11CuuekxyaYMiFuMKGgQrePYMX5bXKLhLG3sDI4CsGAYHPaT7VV7+g=="],
@@ -2790,8 +2707,6 @@
"stylelint": ["stylelint@16.16.0", "", { "dependencies": { "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "@csstools/media-query-list-parser": "^4.0.2", "@csstools/selector-specificity": "^5.0.0", "@dual-bundle/import-meta-resolve": "^4.1.0", "balanced-match": "^2.0.0", "colord": "^2.9.3", "cosmiconfig": "^9.0.0", "css-functions-list": "^3.2.3", "css-tree": "^3.1.0", "debug": "^4.3.7", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", "file-entry-cache": "^10.0.7", "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", "html-tags": "^3.3.1", "ignore": "^7.0.3", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", "known-css-properties": "^0.35.0", "mathml-tag-names": "^2.1.3", "meow": "^13.2.0", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.5.3", "postcss-resolve-nested-selector": "^0.1.6", "postcss-safe-parser": "^7.0.1", "postcss-selector-parser": "^7.1.0", "postcss-value-parser": "^4.2.0", "resolve-from": "^5.0.0", "string-width": "^4.2.3", "supports-hyperlinks": "^3.2.0", "svg-tags": "^1.0.0", "table": "^6.9.0", "write-file-atomic": "^5.0.1" }, "bin": { "stylelint": "bin/stylelint.mjs" } }, "sha512-40X5UOb/0CEFnZVEHyN260HlSSUxPES+arrUphOumGWgXERHfwCD0kNBVILgQSij8iliYVwlc0V7M5bcLP9vPg=="],
"stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="],
"sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
@@ -2896,8 +2811,6 @@
"ufo": ["ufo@1.5.4", "", {}, "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ=="],
"uhyphen": ["uhyphen@0.2.0", "", {}, "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA=="],
"uid-promise": ["uid-promise@1.0.0", "", {}, "sha512-R8375j0qwXyIu/7R0tjdF06/sElHqbmdmWC9M2qQHpEVbvE4I5+38KJI7LUUmQMp7NVq4tKHiBMkT0NFM453Ig=="],
"undici": ["undici@5.28.4", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g=="],
@@ -2916,16 +2829,12 @@
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
"unist-util-remove": ["unist-util-remove@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg=="],
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
"unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="],
"universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="],
"universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
@@ -3740,8 +3649,6 @@
"@node-minify/core/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
"@opennextjs/aws/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
"@opennextjs/aws/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
"@radix-ui/react-collection/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
@@ -4088,8 +3995,6 @@
"@vercel/static-config/ajv": ["ajv@8.6.3", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw=="],
"@vue/compiler-core/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"@vue/compiler-sfc/postcss": ["postcss@8.4.47", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.0", "source-map-js": "^1.2.1" } }, "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ=="],
"@vueuse/integrations/@vueuse/core": ["@vueuse/core@11.2.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.20", "@vueuse/metadata": "11.2.0", "@vueuse/shared": "11.2.0", "vue-demi": ">=0.14.10" } }, "sha512-JIUwRcOqOWzcdu1dGlfW04kaJhW3EXnnjJJfLTtddJanymTL7lF1C0+dVVZ/siLfc73mWn+cGP1PE1PKPruRSA=="],
@@ -4138,8 +4043,6 @@
"decamelize-keys/map-obj": ["map-obj@1.0.1", "", {}, "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg=="],
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"edge-runtime/async-listen": ["async-listen@3.0.1", "", {}, "sha512-cWMaNwUJnf37C/S5TfCkk/15MwbPRwVYALA2jtjkbHjCmAPiDXyNJy2q3p1KAZzDLHAWyarUWSujUoHR4pEgrA=="],
"edge-runtime/picocolors": ["picocolors@1.0.0", "", {}, "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="],
@@ -4188,8 +4091,6 @@
"google-auth-library/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"google-font-metadata/picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"googleapis-common/uuid": ["uuid@7.0.3", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg=="],
"got/get-stream": ["get-stream@4.1.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w=="],
@@ -4214,20 +4115,14 @@
"make-dir/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"mdast-util-gfm-footnote/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="],
"mdast-util-gfm/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
"mdast-util-gfm-footnote/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
"mdast-util-gfm-strikethrough/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="],
"mdast-util-gfm-strikethrough/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
"mdast-util-gfm-table/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="],
"mdast-util-gfm-table/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
"mdast-util-gfm-task-list-item/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="],
"mdast-util-gfm-task-list-item/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
"meow/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
@@ -4258,8 +4153,6 @@
"package-json/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"parse5/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"path-match/path-to-regexp": ["path-to-regexp@1.9.0", "", { "dependencies": { "isarray": "0.0.1" } }, "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g=="],
"path-scurry/lru-cache": ["lru-cache@11.0.2", "", {}, "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA=="],
@@ -4298,10 +4191,6 @@
"read-yaml-file/pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="],
"remark-gfm/mdast-util-gfm": ["mdast-util-gfm@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw=="],
"remark-parse/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="],
"remark-stringify/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
"rimraf/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
@@ -4352,8 +4241,6 @@
"ts-node/arg": ["arg@4.1.0", "", {}, "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="],
"unenv/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"update-notifier/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="],
"url-parse-lax/prepend-http": ["prepend-http@2.0.0", "", {}, "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA=="],
@@ -5132,10 +5019,6 @@
"read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"remark-gfm/mdast-util-gfm/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="],
"remark-gfm/mdast-util-gfm/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
"rimraf/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
+3 -7
View File
@@ -7,9 +7,10 @@
"turbo": "^2.5.0",
"vercel": "^39.3.0"
},
"packageManager": "bun@1.2.15",
"packageManager": "bun@1.2.11",
"overrides": {
"@codemirror/state": "6.4.1",
"@gitbook/api": "^0.117.1",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
@@ -33,12 +34,7 @@
"download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local",
"clean": "turbo run clean"
},
"workspaces": {
"packages": ["packages/*"],
"catalog": {
"@gitbook/api": "^0.120.0"
}
},
"workspaces": ["packages/*"],
"patchedDependencies": {
"decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch",
"@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch"
+1 -1
View File
@@ -10,7 +10,7 @@
},
"version": "0.3.1",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/api": "^0.117.1",
"assert-never": "^1.2.1"
},
"devDependencies": {
-2
View File
@@ -1,2 +0,0 @@
dist/
src/data/*.json
-3
View File
@@ -1,3 +0,0 @@
# `@gitbook/fonts`
Utilities to lookup default fonts supported by GitBook.
-91
View File
@@ -1,91 +0,0 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { APIv2 } from 'google-font-metadata';
import { CustomizationDefaultFont } from '@gitbook/api';
import type { FontDefinitions } from '../src/types';
const googleFontsMap: { [fontName in CustomizationDefaultFont]: string } = {
[CustomizationDefaultFont.Inter]: 'inter',
[CustomizationDefaultFont.FiraSans]: 'fira-sans-extra-condensed',
[CustomizationDefaultFont.IBMPlexSerif]: 'ibm-plex-serif',
[CustomizationDefaultFont.Lato]: 'lato',
[CustomizationDefaultFont.Merriweather]: 'merriweather',
[CustomizationDefaultFont.NotoSans]: 'noto-sans',
[CustomizationDefaultFont.OpenSans]: 'open-sans',
[CustomizationDefaultFont.Overpass]: 'overpass',
[CustomizationDefaultFont.Poppins]: 'poppins',
[CustomizationDefaultFont.Raleway]: 'raleway',
[CustomizationDefaultFont.Roboto]: 'roboto',
[CustomizationDefaultFont.RobotoSlab]: 'roboto-slab',
[CustomizationDefaultFont.SourceSansPro]: 'source-sans-3',
[CustomizationDefaultFont.Ubuntu]: 'ubuntu',
[CustomizationDefaultFont.ABCFavorit]: 'inter',
};
/**
* Scripts to generate the list of all icons.
*/
async function main() {
// @ts-expect-error - we build the object
const output: FontDefinitions = {};
for (const font of Object.values(CustomizationDefaultFont)) {
const googleFontName = googleFontsMap[font];
const fontMetadata = APIv2[googleFontName.toLowerCase()];
if (!fontMetadata) {
throw new Error(`Font ${googleFontName} not found`);
}
output[font] = {
font: googleFontName,
unicodeRange: fontMetadata.unicodeRange,
variants: {
'400': {},
'700': {},
},
};
Object.keys(output[font].variants).forEach((weight) => {
const variants = fontMetadata.variants[weight];
const normalVariant = variants.normal;
if (!normalVariant) {
throw new Error(`Font ${googleFontName} has no normal variant`);
}
output[font].variants[weight] = {};
Object.entries(normalVariant).forEach(([script, url]) => {
output[font].variants[weight][script] = url.url.woff;
});
});
}
await writeDataFile('fonts', JSON.stringify(output, null, 2));
}
/**
* We write both in dist and src as the build process might have happen already
* and tsc doesn't copy the files.
*/
async function writeDataFile(name, content) {
const srcData = path.resolve(__dirname, '../src/data');
const distData = path.resolve(__dirname, '../dist/data');
// Ensure the directories exists
await Promise.all([
fs.mkdir(srcData, { recursive: true }),
fs.mkdir(distData, { recursive: true }),
]);
await Promise.all([
fs.writeFile(path.resolve(srcData, `${name}.json`), content),
fs.writeFile(path.resolve(distData, `${name}.json`), content),
]);
}
main().catch((error) => {
console.error(`Error generating icons list: ${error}`);
process.exit(1);
});
-31
View File
@@ -1,31 +0,0 @@
{
"name": "@gitbook/fonts",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"development": "./src/index.ts",
"default": "./dist/index.js"
}
},
"version": "0.0.0",
"dependencies": {
"@gitbook/api": "catalog:"
},
"devDependencies": {
"google-font-metadata": "^6.0.3",
"typescript": "^5.5.3"
},
"scripts": {
"generate": "bun ./bin/generate.js",
"build": "tsc --project tsconfig.build.json",
"typecheck": "tsc --noEmit",
"dev": "tsc -w",
"clean": "rm -rf ./dist && rm -rf ./src/data",
"unit": "bun test"
},
"files": ["dist", "src", "bin", "README.md", "CHANGELOG.md"],
"engines": {
"node": ">=20.0.0"
}
}
@@ -1,57 +0,0 @@
// Bun Snapshot v1, https://goo.gl/fbAQLP
exports[`getDefaultFont should return correct object for Latin text 1`] = `
{
"font": "Inter",
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
}
`;
exports[`getDefaultFont should return correct object for Cyrillic text 1`] = `
{
"font": "Inter",
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff",
}
`;
exports[`getDefaultFont should return correct object for Greek text 1`] = `
{
"font": "Inter",
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZxhjp-Ek-_0ewmM.woff",
}
`;
exports[`getDefaultFont should handle mixed script text 1`] = `
{
"font": "Inter",
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff",
}
`;
exports[`getDefaultFont should handle different font weights: regular 1`] = `
{
"font": "Inter",
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
}
`;
exports[`getDefaultFont should handle different font weights: bold 1`] = `
{
"font": "Inter",
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYAZ9hjp-Ek-_0ew.woff",
}
`;
exports[`getDefaultFont should handle different fonts: inter 1`] = `
{
"font": "Inter",
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
}
`;
exports[`getDefaultFont should handle different fonts: roboto 1`] = `
{
"font": "Roboto",
"url": "https://fonts.gstatic.com/s/roboto/v32/KFOmCnqEu92Fr1Mu4mxMKTU1Kg.woff",
}
`;
-5
View File
@@ -1,5 +0,0 @@
import type { FontDefinitions } from './types';
import rawFonts from './data/fonts.json' with { type: 'json' };
export const fonts: FontDefinitions = rawFonts;
-119
View File
@@ -1,119 +0,0 @@
import { describe, expect, it } from 'bun:test';
import { CustomizationDefaultFont } from '@gitbook/api';
import { getDefaultFont } from './getDefaultFont';
describe('getDefaultFont', () => {
it('should return null for invalid font', () => {
const result = getDefaultFont({
font: 'invalid-font' as CustomizationDefaultFont,
text: 'Hello',
weight: 400,
});
expect(result).toBeNull();
});
it('should return null for invalid weight', () => {
const result = getDefaultFont({
font: CustomizationDefaultFont.Inter,
text: 'Hello',
weight: 999 as any,
});
expect(result).toBeNull();
});
it('should return null for text not supported by any script', () => {
const result = getDefaultFont({
font: CustomizationDefaultFont.Inter,
text: '😀', // Emoji not supported by Inter
weight: 400,
});
expect(result).toBeNull();
});
it('should return correct object for Latin text', () => {
const result = getDefaultFont({
font: CustomizationDefaultFont.Inter,
text: 'Hello World',
weight: 400,
});
expect(result).not.toBeNull();
expect(result?.font).toBe(CustomizationDefaultFont.Inter);
expect(result).toMatchSnapshot();
});
it('should return correct object for Cyrillic text', () => {
const result = getDefaultFont({
font: CustomizationDefaultFont.Inter,
text: 'Привет мир',
weight: 400,
});
expect(result).not.toBeNull();
expect(result?.font).toBe(CustomizationDefaultFont.Inter);
expect(result).toMatchSnapshot();
});
it('should return correct object for Greek text', () => {
const result = getDefaultFont({
font: CustomizationDefaultFont.Inter,
text: 'Γεια σας',
weight: 400,
});
expect(result).not.toBeNull();
expect(result?.font).toBe(CustomizationDefaultFont.Inter);
expect(result).toMatchSnapshot();
});
it('should handle mixed script text', () => {
const result = getDefaultFont({
font: CustomizationDefaultFont.Inter,
text: 'Hello Привет',
weight: 400,
});
expect(result).not.toBeNull();
expect(result?.font).toBe(CustomizationDefaultFont.Inter);
expect(result).toMatchSnapshot();
});
it('should handle different font weights', () => {
const regular = getDefaultFont({
font: CustomizationDefaultFont.Inter,
text: 'Hello',
weight: 400,
});
const bold = getDefaultFont({
font: CustomizationDefaultFont.Inter,
text: 'Hello',
weight: 700,
});
expect(regular).not.toBeNull();
expect(bold).not.toBeNull();
expect(regular).toMatchSnapshot('regular');
expect(bold).toMatchSnapshot('bold');
});
it('should handle empty string', () => {
const result = getDefaultFont({
font: CustomizationDefaultFont.Inter,
text: '',
weight: 400,
});
expect(result).toBeNull();
});
it('should handle different fonts', () => {
const inter = getDefaultFont({
font: CustomizationDefaultFont.Inter,
text: 'Hello',
weight: 400,
});
const roboto = getDefaultFont({
font: CustomizationDefaultFont.Roboto,
text: 'Hello',
weight: 400,
});
expect(inter).not.toBeNull();
expect(roboto).not.toBeNull();
expect(inter).toMatchSnapshot('inter');
expect(roboto).toMatchSnapshot('roboto');
});
});
-112
View File
@@ -1,112 +0,0 @@
import type { CustomizationDefaultFont } from '@gitbook/api';
import { fonts } from './fonts';
import type { FontWeight } from './types';
/**
* Get the URL to load a font for a text.
*/
export function getDefaultFont(input: {
/**
* GitBook font to use.
*/
font: CustomizationDefaultFont;
/**
* Text to display with the font.
*/
text: string;
/**
* Font weight to use.
*/
weight: FontWeight;
}): { font: string; url: string } | null {
if (!input.text.trim()) {
return null;
}
const fontDefinition = fonts[input.font];
if (!fontDefinition) {
return null;
}
const variant = fontDefinition.variants[`${input.weight}`];
if (!variant) {
return null;
}
const script = getBestUnicodeRange(input.text, fontDefinition.unicodeRange);
if (!script) {
return null;
}
return variant[script]
? {
font: input.font,
url: variant[script],
}
: null;
}
/**
* Determine which named @font-face unicode-range covers
* the greatest share of the characters in `text`.
*
* @param text The text you want to inspect.
* @param ranges An object whose keys are range names and whose
* values are CSS-style comma-separated unicode-range
* declarations (e.g. "U+0370-03FF,U+1F00-1FFF").
* @returns The key of the best-matching range, or `null`
* when nothing matches at all.
*/
function getBestUnicodeRange(text: string, ranges: Record<string, string>): string | null {
// ---------- helper: parse "U+XXXX" or "U+XXXX-YYYY" ----------
const parseOne = (token: string): [number, number] | null => {
token = token.trim().toUpperCase();
if (!token.startsWith('U+')) return null;
const body = token.slice(2); // drop "U+"
const [startHex, endHex] = body.split('-');
const start = Number.parseInt(startHex, 16);
const end = endHex ? Number.parseInt(endHex, 16) : start;
if (Number.isNaN(start) || Number.isNaN(end) || end < start) return null;
return [start, end];
};
// ---------- helper: build lookup table ----------
const parsed: Record<string, [number, number][]> = {};
for (const [label, list] of Object.entries(ranges)) {
parsed[label] = list
.split(',')
.map(parseOne)
.filter((x): x is [number, number] => x !== null);
}
// ---------- tally code-point hits ----------
const hits: Record<string, number> = Object.fromEntries(Object.keys(parsed).map((k) => [k, 0]));
for (let i = 0; i < text.length; ) {
const cp = text.codePointAt(i)!;
i += cp > 0xffff ? 2 : 1; // advance by 1 UTF-16 code-unit (or 2 for surrogates)
for (const [label, rangesArr] of Object.entries(parsed)) {
if (rangesArr.some(([lo, hi]) => cp >= lo && cp <= hi)) {
hits[label]++;
}
}
}
// ---------- choose the "best" ----------
let winner: string | null = null;
let maxCount = 0;
for (const [label, count] of Object.entries(hits)) {
if (count > maxCount) {
maxCount = count;
winner = label;
}
}
return maxCount > 0 ? winner : null;
}
-2
View File
@@ -1,2 +0,0 @@
export * from './getDefaultFont';
export * from './types';
-17
View File
@@ -1,17 +0,0 @@
import type { CustomizationDefaultFont } from '@gitbook/api';
export type FontWeight = 400 | 700;
export type FontDefinition = {
font: string;
unicodeRange: {
[script: string]: string;
};
variants: {
[weight in string]: {
[script: string]: string;
};
};
};
export type FontDefinitions = { [fontName in CustomizationDefaultFont]: FontDefinition };
-4
View File
@@ -1,4 +0,0 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "src/**/*.test.ts"]
}
-24
View File
@@ -1,24 +0,0 @@
{
"compilerOptions": {
"lib": ["es2023"],
"module": "ESNext",
"target": "es2022",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowJs": true,
"noEmit": false,
"declaration": true,
"outDir": "dist",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"types": [
"bun-types" // add Bun global
]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
-9
View File
@@ -1,9 +0,0 @@
{
"extends": ["//"],
"tasks": {
"generate": {
"inputs": ["bin/**/*", "package.json"],
"outputs": ["src/data/*.json", "dist/data/*.json"]
}
}
}
@@ -22,12 +22,6 @@
"bucket_name": "gitbook-open-v2-cache-preview"
}
],
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "gitbook-open-v2-preview"
}
],
"durable_objects": {
"bindings": [
{
@@ -67,12 +61,6 @@
"service": "gitbook-x-staging-tail"
}
],
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "gitbook-open-v2-staging"
}
],
"durable_objects": {
"bindings": [
{
@@ -112,12 +100,6 @@
"service": "gitbook-x-prod-tail"
}
],
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "gitbook-open-v2-production"
}
],
"durable_objects": {
"bindings": [
{
@@ -49,20 +49,17 @@ class GitbookIncrementalCache implements IncrementalCache {
const localCacheEntry = await localCache.match(this.getCacheUrlKey(cacheKey));
if (localCacheEntry) {
span.setAttribute('cacheHit', 'local');
const result = (await localCacheEntry.json()) as WithLastModified<
CacheValue<CacheType>
>;
return this.returnNullOn404(result);
return localCacheEntry.json();
}
const r2Object = await r2.get(cacheKey);
if (!r2Object) return null;
span.setAttribute('cacheHit', 'r2');
return this.returnNullOn404({
return {
value: await r2Object.json(),
lastModified: r2Object.uploaded.getTime(),
});
};
} catch (e) {
console.error('Failed to get from cache', e);
return null;
@@ -71,18 +68,6 @@ class GitbookIncrementalCache implements IncrementalCache {
);
}
//TODO: This is a workaround to handle 404 responses in the cache.
// It should be handled by OpenNext cache interception directly. This should be removed once OpenNext cache interception is fixed.
returnNullOn404<CacheType extends CacheEntryType = 'cache'>(
cacheEntry: WithLastModified<CacheValue<CacheType>> | null
): WithLastModified<CacheValue<CacheType>> | null {
if (!cacheEntry?.value) return null;
if ('meta' in cacheEntry.value && cacheEntry.value.meta?.status === 404) {
return null;
}
return cacheEntry;
}
async set<CacheType extends CacheEntryType = 'cache'>(
key: string,
value: CacheValue<CacheType>,
+2 -2
View File
@@ -3,9 +3,9 @@
"version": "0.3.0",
"private": true,
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/api": "^0.117.1",
"@gitbook/cache-tags": "workspace:*",
"@opennextjs/cloudflare": "1.2.1",
"@opennextjs/cloudflare": "1.1.0",
"@sindresorhus/fnv1a": "^3.1.0",
"assert-never": "^1.2.1",
"jwt-decode": "^4.0.0",
@@ -1,14 +0,0 @@
import type { NextRequest } from 'next/server';
import { serveLLMsFullTxt } from '@/routes/llms-full';
import { type RouteLayoutParams, getStaticSiteContext } from '@v2/app/utils';
export const dynamic = 'force-static';
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<RouteLayoutParams> }
) {
const { context } = await getStaticSiteContext(await params);
return serveLLMsFullTxt(context);
}
-3
View File
@@ -1,5 +1,4 @@
import { getVisitorAuthClaims, getVisitorAuthClaimsFromToken } from '@/lib/adaptive';
import { getDynamicCustomizationSettings } from '@/lib/customization';
import type { SiteAPIToken } from '@gitbook/api';
import { type SiteURLData, fetchSiteContextByURLLookup, getBaseContext } from '@v2/lib/context';
import { jwtDecode } from 'jwt-decode';
@@ -68,8 +67,6 @@ export async function getDynamicSiteContext(params: RouteLayoutParams) {
siteURLData
);
context.customization = await getDynamicCustomizationSettings(context.customization);
return {
context,
visitorAuthClaims: getVisitorAuthClaims(siteURLData),
File diff suppressed because it is too large Load Diff
@@ -11,3 +11,15 @@ export function getCloudflareContext() {
return getCloudflareContextOpenNext();
}
/**
* Return an object representing the current request.
*/
export function getCloudflareRequestGlobal() {
const context = getCloudflareContext();
if (!context) {
return null;
}
return context.cf;
}
+1 -1
View File
@@ -1,7 +1,7 @@
export * from './api';
export * from './types';
export * from './pages';
export * from './urls';
export * from './errors';
export * from './lookup';
export * from './visitor';
export * from './pages';
@@ -0,0 +1,52 @@
import { describe, expect, it, mock } from 'bun:test';
import { AsyncLocalStorage } from 'node:async_hooks';
import { withCacheKey, withoutConcurrentExecution } from './memoize';
describe('withoutConcurrentExecution', () => {
it('should memoize the function based on the cache key', async () => {
const fn = mock(async (_cacheKey: string, a: number, b: number) => a + b);
const memoized = withoutConcurrentExecution(() => null, fn);
const p1 = memoized('c1', 1, 2);
const p2 = memoized('c1', 1, 2);
const p3 = memoized('c3', 2, 3);
expect(await p1).toBe(await p2);
expect(await p1).not.toBe(await p3);
expect(fn.mock.calls.length).toBe(2);
});
it('should support caching per request', async () => {
const fn = mock(async () => Math.random());
const request1 = { id: 'request1' };
const request2 = { id: 'request2' };
const requestContext = new AsyncLocalStorage<{ id: string }>();
const memoized = withoutConcurrentExecution(() => requestContext.getStore(), fn);
// Both in the same request
const promise1 = requestContext.run(request1, () => memoized('c1'));
const promise2 = requestContext.run(request1, () => memoized('c1'));
// In a different request
const promise3 = requestContext.run(request2, () => memoized('c1'));
expect(await promise1).toBe(await promise2);
expect(await promise1).not.toBe(await promise3);
expect(fn.mock.calls.length).toBe(2);
});
});
describe('withCacheKey', () => {
it('should wrap the function by passing the cache key', async () => {
const fn = mock(
async (cacheKey: string, arg: { a: number; b: number }, c: number) =>
`${cacheKey}, result=${arg.a + arg.b + c}`
);
const memoized = withCacheKey(fn);
expect(await memoized({ a: 1, b: 2 }, 4)).toBe('[[["a",1],["b",2]],4], result=7');
expect(fn.mock.calls.length).toBe(1);
});
});
+2 -4
View File
@@ -1,17 +1,15 @@
import type { GitBookDataFetcher } from './types';
/**
* Wrap a function by preventing concurrent executions of the same function.
* With a logic to work per-request in Cloudflare Workers.
*/
export function withoutConcurrentExecution<ArgsType extends any[], ReturnType>(
dataFetcherObject: GitBookDataFetcher,
getGlobalContext: () => object | null | undefined,
wrapped: (key: string, ...args: ArgsType) => Promise<ReturnType>
): (cacheKey: string, ...args: ArgsType) => Promise<ReturnType> {
const globalPromiseCache = new WeakMap<object, Map<string, Promise<ReturnType>>>();
return (key: string, ...args: ArgsType) => {
const globalContext = dataFetcherObject;
const globalContext = getGlobalContext() ?? globalThis;
/**
* Cache storage that is scoped to the current request when executed in Cloudflare Workers,
+4 -44
View File
@@ -1,30 +1,15 @@
import { waitUntil } from '@/lib/waitUntil';
import type { JSONDocument, RevisionPageDocument } from '@gitbook/api';
import type { GitBookSiteContext, GitBookSpaceContext } from '../context';
import type { JSONDocument, RevisionPageDocument, Space } from '@gitbook/api';
import { getDataOrNull } from './errors';
import type { GitBookDataFetcher } from './types';
/**
* Get the document for a page.
*/
export async function getPageDocument(
context: GitBookSpaceContext | GitBookSiteContext,
dataFetcher: GitBookDataFetcher,
space: Space,
page: RevisionPageDocument
): Promise<JSONDocument | null> {
const { dataFetcher, space } = context;
if (
'site' in context &&
(context.site.id === 'site_JOVzv' || context.site.id === 'site_IxAYj')
) {
return getDataOrNull(
dataFetcher.getRevisionPageDocument({
spaceId: space.id,
revisionId: space.revision,
pageId: page.id,
})
);
}
if (page.documentId) {
return getDataOrNull(
dataFetcher.getDocument({ spaceId: space.id, documentId: page.documentId })
@@ -41,30 +26,5 @@ export async function getPageDocument(
);
}
// Pre-fetch the document to start filling the cache before we migrate to this API.
if (isInPercentRollout(space.id, 10)) {
await waitUntil(
getDataOrNull(
dataFetcher.getRevisionPageDocument({
spaceId: space.id,
revisionId: space.revision,
pageId: page.id,
})
)
);
}
return null;
}
function isInPercentRollout(value: string, rollout: number) {
return getRandomPercent(value) < rollout;
}
function getRandomPercent(value: string) {
const hash = value.split('').reduce((acc, char) => {
return acc + char.charCodeAt(0);
}, 0);
return hash % 100;
}
+2 -9
View File
@@ -106,15 +106,6 @@ export interface GitBookDataFetcher {
pageId: string;
}): Promise<DataFetcherResponse<string>>;
/**
* Get the document of a page by its path.
*/
getRevisionPageDocument(params: {
spaceId: string;
revisionId: string;
pageId: string;
}): Promise<DataFetcherResponse<api.JSONDocument>>;
/**
* Get a document by its space ID and document ID.
*/
@@ -198,5 +189,7 @@ export interface GitBookDataFetcher {
input: api.AIMessageInput[];
output: api.AIOutputFormat;
model: api.AIModel;
tools?: api.AIToolCapabilities;
previousResponseId?: string;
}): AsyncGenerator<api.AIStreamResponse, void, unknown>;
}
+1 -4
View File
@@ -41,8 +41,6 @@ const ADAPTIVE_CONTENT_HOSTS = [
'docs.gitbook.com',
'adaptive-docs.gitbook-staging.com',
'enriched-content-playground.gitbook-staging.io',
'docs.testgitbook.com',
'launchdarkly-site.gitbook.education',
];
export async function middleware(request: NextRequest) {
@@ -228,8 +226,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
const customization = siteRequestURL.searchParams.get('customization');
if (customization && validateSerializedCustomization(customization)) {
routeType = 'dynamic';
// We need to encode the customization headers, otherwise it will fail for some customization values containing non ASCII chars on vercel.
requestHeaders.set(MiddlewareHeaders.Customization, encodeURIComponent(customization));
requestHeaders.set(MiddlewareHeaders.Customization, customization);
}
const theme = siteRequestURL.searchParams.get('theme');
if (theme === CustomizationThemeMode.Dark || theme === CustomizationThemeMode.Light) {
+5 -5
View File
@@ -204,11 +204,11 @@ const testCases: TestsCase[] = [
contentBaseURL: 'https://sosovalue-white-paper.gitbook.io',
tests: [{ name: 'Home', url: '/' }],
},
// {
// name: 'docs.revrobotics.com',
// contentBaseURL: 'https://docs.revrobotics.com',
// tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
// },
{
name: 'docs.revrobotics.com',
contentBaseURL: 'https://docs.revrobotics.com',
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
},
{
name: 'chartschool.stockcharts.com',
contentBaseURL: 'https://chartschool.stockcharts.com',
-55
View File
@@ -1,11 +1,9 @@
import {
CustomizationBackground,
CustomizationCorners,
CustomizationDepth,
CustomizationHeaderPreset,
CustomizationIconsStyle,
CustomizationSidebarListStyle,
CustomizationThemeMode,
} from '@gitbook/api';
import { expect } from '@playwright/test';
import jwt from 'jsonwebtoken';
@@ -432,38 +430,6 @@ const testCases: TestsCase[] = [
},
],
},
{
name: 'llms.txt',
skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
tests: [
{
name: 'llms.txt',
url: 'llms.txt',
screenshot: false,
run: async (_page, response) => {
expect(response?.status()).toBe(200);
expect(response?.headers()['content-type']).toContain('text/markdown');
},
},
],
},
{
name: 'llms-full.txt',
skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
tests: [
{
name: 'llms-full.txt',
url: 'llms-full.txt',
screenshot: false,
run: async (_page, response) => {
expect(response?.status()).toBe(200);
expect(response?.headers()['content-type']).toContain('text/markdown');
},
},
],
},
{
name: 'Site subdirectory (proxy)',
skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
@@ -666,7 +632,6 @@ const testCases: TestsCase[] = [
name: 'Stepper',
url: 'blocks/stepper',
},
{ name: 'Columns', url: 'blocks/columns' },
],
},
{
@@ -683,16 +648,6 @@ const testCases: TestsCase[] = [
url: 'page-options/page-with-cover',
run: waitForCookiesDialog,
},
{
name: 'With cover for dark mode',
url: `page-options/page-with-dark-cover${getCustomizationURL({
themes: {
default: CustomizationThemeMode.Dark,
toggeable: false,
},
})}`,
run: waitForCookiesDialog,
},
{
name: 'With hero cover',
url: 'page-options/page-with-hero-cover',
@@ -862,16 +817,6 @@ const testCases: TestsCase[] = [
}),
run: waitForCookiesDialog,
},
{
name: `With flat and circular corners - Theme mode ${themeMode}`,
url: getCustomizationURL({
styling: {
depth: CustomizationDepth.Flat,
corners: CustomizationCorners.Circular,
},
}),
run: waitForCookiesDialog,
},
]),
},
{
-2
View File
@@ -3,7 +3,6 @@ import {
CustomizationBackground,
CustomizationCorners,
CustomizationDefaultFont,
CustomizationDepth,
type CustomizationHeaderItem,
CustomizationHeaderPreset,
CustomizationIconsStyle,
@@ -279,7 +278,6 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
dangerColor: { light: '#FB2C36', dark: '#FB2C36' },
successColor: { light: '#00C950', dark: '#00C950' },
corners: CustomizationCorners.Rounded,
depth: CustomizationDepth.Subtle,
font: CustomizationDefaultFont.Inter,
background: CustomizationBackground.Plain,
icons: CustomizationIconsStyle.Regular,
+3 -11
View File
@@ -10,19 +10,18 @@
"typecheck": "tsc --noEmit",
"e2e": "playwright test e2e/internal.spec.ts",
"e2e-customers": "playwright test e2e/customers.spec.ts",
"unit": "bun test {src,packages} --preload ./tests/preload-bun.ts",
"unit": "bun test {src,packages}",
"generate": "gitbook-icons ./public/~gitbook/static/icons custom-icons && gitbook-math ./public/~gitbook/static/math",
"copy:icons": "gitbook-icons ./public/~gitbook/static/icons",
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math"
},
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/api": "^0.117.1",
"@gitbook/cache-do": "workspace:*",
"@gitbook/cache-tags": "workspace:*",
"@gitbook/colors": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*",
"@gitbook/icons": "workspace:*",
"@gitbook/fonts": "workspace:*",
"@gitbook/openapi-parser": "workspace:*",
"@gitbook/react-contentkit": "workspace:*",
"@gitbook/react-math": "workspace:*",
@@ -47,20 +46,13 @@
"katex": "^0.16.9",
"mathjax": "^3.2.2",
"mdast-util-to-markdown": "^2.1.2",
"mdast-util-from-markdown": "^2.0.2",
"mdast-util-frontmatter": "^2.0.1",
"mdast-util-gfm": "^3.1.0",
"micromark-extension-gfm": "^3.0.0",
"micromark-extension-frontmatter": "^2.0.0",
"unist-util-remove": "^4.0.0",
"unist-util-visit": "^5.0.0",
"memoizee": "^0.4.17",
"next": "14.2.26",
"next-themes": "^0.2.1",
"nuqs": "^2.2.3",
"object-hash": "^3.0.0",
"openapi-types": "^12.1.3",
"p-map": "^7.0.3",
"p-map": "^7.0.0",
"parse-cache-control": "^1.0.1",
"partial-json": "^0.1.7",
"react": "^19.0.0",
@@ -0,0 +1,31 @@
import type { AIMessage } from '@gitbook/api';
import { DocumentView } from '../DocumentView';
/**
* Render a message from the API backend.
*/
export function MessageView(props: {
message: AIMessage;
}) {
const { message } = props;
return (
<div className="flex flex-col gap-2">
{message.steps.map((step, index) => {
return (
<div key={index} className="flex flex-col gap-2">
<DocumentView
document={step.content}
context={{
mode: 'default',
contentContext: undefined,
wrapBlocksInSuspense: false,
}}
style={['space-y-5']}
/>
</div>
);
})}
</div>
);
}
@@ -1,124 +0,0 @@
'use server';
import { type AIMessageInput, AIModel, type AIStreamResponse } from '@gitbook/api';
import type { GitBookBaseContext } from '@v2/lib/context';
import { EventIterator } from 'event-iterator';
import type { MaybePromise } from 'p-map';
import * as partialJson from 'partial-json';
import type { DeepPartial } from 'ts-essentials';
import type { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
/**
* Get the latest value from a stream and the response id.
*/
export async function generate<T>(
promise: MaybePromise<{
stream: EventIterator<T>;
response: Promise<{ responseId: string }>;
}>
) {
const input = await promise;
let value: T | undefined;
for await (const event of input.stream) {
value = event;
}
const { responseId } = await input.response;
return {
responseId,
value,
};
}
/**
* Stream the generation of an object using the AI.
*/
export async function streamGenerateObject<T>(
context: GitBookBaseContext,
{
organizationId,
siteId,
}: {
organizationId: string;
siteId: string;
},
{
schema,
messages,
model = AIModel.Fast,
}: {
schema: z.ZodSchema<T>;
messages: AIMessageInput[];
model?: AIModel;
previousResponseId?: string;
}
) {
const rawStream = context.dataFetcher.streamAIResponse({
organizationId,
siteId,
input: messages,
output: {
type: 'object',
schema: zodToJsonSchema(schema),
},
model,
});
let json = '';
return parseResponse<DeepPartial<T>>(rawStream, (event) => {
if (event.type === 'response_object') {
json += event.jsonChunk;
const parsed = partialJson.parse(json, partialJson.ALL);
return parsed;
}
});
}
/**
* Parse a stream from the API to extract the responseId.
*/
function parseResponse<T>(
responseStream: EventIterator<AIStreamResponse>,
parse: (response: AIStreamResponse) => T | undefined
): {
stream: EventIterator<T>;
response: Promise<{ responseId: string }>;
} {
let resolveResponse: (value: { responseId: string }) => void;
const response = new Promise<{ responseId: string }>((resolve) => {
resolveResponse = resolve;
});
const stream = new EventIterator<T>((queue) => {
(async () => {
let foundResponse = false;
for await (const event of responseStream) {
if (event.type === 'response_finish') {
foundResponse = true;
resolveResponse({ responseId: event.responseId });
} else {
const parsed = parse(event);
if (parsed !== undefined) {
queue.push(parsed);
}
}
}
if (!foundResponse) {
throw new Error('No response found');
}
})().then(
() => {
queue.stop();
},
(error) => {
queue.fail(error);
}
);
});
return { stream, response };
}
@@ -0,0 +1,214 @@
'use server';
import {
type AIMessage,
AIMessageRole,
type AIMessageStep,
type AIStreamResponse,
} from '@gitbook/api';
import type { GitBookBaseContext } from '@v2/lib/context';
import type { GitBookDataFetcher } from '@v2/lib/data';
import { EventIterator } from 'event-iterator';
import type { MaybePromise } from 'p-map';
import * as partialJson from 'partial-json';
import type { DeepPartial } from 'ts-essentials';
import type { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { MessageView } from '../MessageView';
/**
* Get the latest value from a stream and the response id.
*/
export async function generate<T>(
promise: MaybePromise<{
stream: EventIterator<T>;
response: Promise<{ responseId: string }>;
}>
) {
const input = await promise;
let value: T | undefined;
for await (const event of input.stream) {
value = event;
}
const { responseId } = await input.response;
return {
responseId,
value,
};
}
/**
* Stream the generation of an object using the AI.
*/
export async function streamGenerateObject<T>(
context: GitBookBaseContext,
{
schema,
...input
}: Omit<Parameters<GitBookDataFetcher['streamAIResponse']>[0], 'output'> & {
schema: z.ZodSchema<T>;
}
) {
const rawStream = context.dataFetcher.streamAIResponse({
...input,
output: {
type: 'object',
schema: zodToJsonSchema(schema),
},
});
let json = '';
return parseResponse<DeepPartial<T>>(rawStream, (event) => {
if (event.type === 'response_object') {
json += event.jsonChunk;
const parsed = partialJson.parse(json, partialJson.ALL);
return parsed;
}
});
}
/**
* Stream the generation of a document.
*/
export async function streamGenerateDocument(
context: GitBookBaseContext,
input: Omit<Parameters<GitBookDataFetcher['streamAIResponse']>[0], 'output'>
) {
const rawStream = context.dataFetcher.streamAIResponse({
...input,
output: {
type: 'document',
},
});
const message: AIMessage = {
id: '',
role: AIMessageRole.Assistant,
steps: [],
};
const updateProcessingMessageStep = (
stepIndex: number,
callback: (step: AIMessageStep) => void
) => {
if (stepIndex > message.steps.length) {
throw new Error(
`Step index out of bounds ${stepIndex} (${message.steps.length} steps)`
);
}
if (message.steps[stepIndex]) {
message.steps = [...message.steps];
message.steps[stepIndex] = { ...message.steps[stepIndex] };
callback(message.steps[stepIndex]);
} else {
message.steps = [
...message.steps,
{
content: {
object: 'document',
data: {},
nodes: [],
},
},
];
callback(message.steps[stepIndex]);
}
};
return parseResponse<React.ReactNode>(rawStream, (event) => {
switch (event.type) {
/**
* The agent is processing a tool call in a new message.
*/
case 'response_tool_call': {
updateProcessingMessageStep(event.stepIndex, (step) => {
step.toolCalls ??= [];
step.toolCalls.push(event.toolCall);
});
break;
}
/**
* The agent is writing the content of a new message.
*/
case 'response_reasoning':
case 'response_document': {
updateProcessingMessageStep(event.stepIndex, (step) => {
const container = event.type === 'response_reasoning' ? 'reasoning' : 'content';
step[container] ??= {
object: 'document',
data: {},
nodes: [],
};
step[container] = {
...step[container],
nodes: [...step[container].nodes],
};
if (event.operation === 'insert') {
step[container].nodes.push(...event.blocks);
} else {
step[container].nodes.splice(
-event.blocks.length,
event.blocks.length,
...event.blocks
);
}
});
break;
}
}
return <MessageView message={message} />;
});
}
/**
* Parse a stream from the API to extract the responseId.
*/
function parseResponse<T>(
responseStream: EventIterator<AIStreamResponse>,
parse: (response: AIStreamResponse) => T | undefined
): {
stream: EventIterator<T>;
response: Promise<{ responseId: string }>;
} {
let resolveResponse: (value: { responseId: string }) => void;
const response = new Promise<{ responseId: string }>((resolve) => {
resolveResponse = resolve;
});
const stream = new EventIterator<T>((queue) => {
(async () => {
let foundResponse = false;
for await (const event of responseStream) {
if (event.type === 'response_finish') {
foundResponse = true;
resolveResponse({ responseId: event.responseId });
} else {
const parsed = parse(event);
if (parsed !== undefined) {
queue.push(parsed);
}
}
}
if (!foundResponse) {
throw new Error('No response found');
}
})().then(
() => {
queue.stop();
},
(error) => {
queue.fail(error);
}
);
});
return { stream, response };
}
@@ -1 +1,2 @@
export * from './streamLinkPageSummary';
export * from './api';
@@ -2,7 +2,7 @@
import { filterOutNullable } from '@/lib/typescript';
import { getV1BaseContext } from '@/lib/v1';
import { isV2 } from '@/lib/v2';
import { AIMessageRole } from '@gitbook/api';
import { AIMessageRole, AIModel } from '@gitbook/api';
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
import { getServerActionBaseContext } from '@v2/lib/server-actions';
import { z } from 'zod';
@@ -32,23 +32,18 @@ export async function* streamLinkPageSummary({
const baseContext = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
const siteURLData = await getSiteURLDataFromMiddleware();
const { stream } = await streamGenerateObject(
baseContext,
{
organizationId: siteURLData.organization,
siteId: siteURLData.site,
},
{
schema: z.object({
highlight: z
.string()
.describe('The reason why the user should read the target page.'),
// questions: z.array(z.string().describe('The questions to sea')).max(3),
}),
messages: [
{
role: AIMessageRole.Developer,
content: `# 1. Role
const { stream } = await streamGenerateObject(baseContext, {
organizationId: siteURLData.organization,
siteId: siteURLData.site,
model: AIModel.Fast,
schema: z.object({
highlight: z.string().describe('The reason why the user should read the target page.'),
// questions: z.array(z.string().describe('The questions to sea')).max(3),
}),
input: [
{
role: AIMessageRole.Developer,
content: `# 1. Role
You are a contextual fact extractor. Your job is to find the exact fact from the linked page that directly answers the implied question in the current paragraph.
# 2. Task
@@ -64,60 +59,60 @@ Extract a contextually-relevant fact that:
3. Ensure the fact relates directly to the context of the paragraph containing the link
4. Avoid ALL instructional language including words like "use", "click", "select", "create"
5. Keep it under 30 words, factual and declarative about what EXISTS or IS TRUE`,
},
{
role: AIMessageRole.Developer,
content: `# 4. Current page
},
{
role: AIMessageRole.Developer,
content: `# 4. Current page
The content of the current page is:`,
attachments: [
{
type: 'page' as const,
spaceId: currentSpaceId,
pageId: currentPageId,
},
],
},
...(visitedPages
? [
{
role: AIMessageRole.Developer,
content: '# 5. Previous pages',
},
...visitedPages.map(({ spaceId, pageId }) => ({
role: AIMessageRole.Developer,
content: `## Page ${pageId}`,
attachments: [
{
type: 'page' as const,
spaceId,
pageId,
},
],
})),
]
: []),
{
role: AIMessageRole.Developer,
content: `# 6. Target page
attachments: [
{
type: 'page' as const,
spaceId: currentSpaceId,
pageId: currentPageId,
},
],
},
...(visitedPages
? [
{
role: AIMessageRole.Developer,
content: '# 5. Previous pages',
},
...visitedPages.map(({ spaceId, pageId }) => ({
role: AIMessageRole.Developer,
content: `## Page ${pageId}`,
attachments: [
{
type: 'page' as const,
spaceId,
pageId,
},
],
})),
]
: []),
{
role: AIMessageRole.Developer,
content: `# 6. Target page
The content of the target page is:`,
attachments: [
{
type: 'page' as const,
spaceId: targetSpaceId,
pageId: targetPageId,
},
],
},
{
role: AIMessageRole.Developer,
content: `# 7. Link preview
attachments: [
{
type: 'page' as const,
spaceId: targetSpaceId,
pageId: targetPageId,
},
],
},
{
role: AIMessageRole.Developer,
content: `# 7. Link preview
The content of the link preview is:
> ${linkPreview}
> Page ID: ${targetPageId}`,
},
{
role: AIMessageRole.Developer,
content: `# 8. Guidelines & Examples
},
{
role: AIMessageRole.Developer,
content: `# 8. Guidelines & Examples
ALWAYS:
- ALWAYS choose facts that directly fulfill the contextual need where the link appears
- ALWAYS connect target page information specifically to the current paragraph context
@@ -146,14 +141,13 @@ Current paragraph: "Your team mentioned issues with conflicting edits. Need to c
Preview: "Live Edit: Real-time collaborative editing."
✓ "Teams with GitHub repositories (like yours) cannot use this feature due to sync limitations."
✗ "Incompatible with GitHub/GitLab sync and requires specific visibility settings."`,
},
{
role: AIMessageRole.User,
content: `I'm considering reading the link titled "${linkTitle}" pointing to page ${targetPageId}. Why should I read it? Relate it to the paragraph I'm currently reading.`,
},
].filter(filterOutNullable),
}
);
},
{
role: AIMessageRole.User,
content: `I'm considering reading the link titled "${linkTitle}" pointing to page ${targetPageId}. Why should I read it? Relate it to the paragraph I'm currently reading.`,
},
].filter(filterOutNullable),
});
for await (const value of stream) {
const highlight = value.highlight;
@@ -34,7 +34,7 @@ export function AnnouncementBanner(props: {
<Tag
href={contentRef?.href ?? ''}
className={tcls(
'flex w-full items-start justify-center overflow-hidden circular-corners:rounded-xl rounded-md straight-corners:rounded-none px-4 py-3 text-neutral-strong text-sm theme-bold:ring-1 theme-gradient:ring-1 ring-inset transition-colors',
'flex w-full items-start justify-center overflow-hidden rounded-md straight-corners:rounded-none px-4 py-3 text-neutral-strong text-sm theme-bold:ring-1 theme-gradient:ring-1 ring-inset transition-colors',
style.container,
closeable && 'pr-12',
hasLink && style.hover
@@ -81,7 +81,7 @@ export function AnnouncementBanner(props: {
</Tag>
{closeable ? (
<button
className={`absolute top-0 right-4 mt-2 mr-2 rounded circular-corners:rounded-lg straight-corners:rounded-none p-1.5 transition-all hover:ring-1 sm:right-6 md:right-8 ${style.close}`}
className={`absolute top-0 right-4 mt-2 mr-2 rounded straight-corners:rounded-none p-1.5 transition-all hover:ring-1 sm:right-6 md:right-8 ${style.close}`}
type="button"
onClick={dismissAnnouncement}
title={tString(language, 'close')}
@@ -47,11 +47,9 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
'bg-tint-base',
'rounded',
'straight-corners:rounded-none',
'circular-corners:rounded-2xl',
'ring-1',
'ring-tint-subtle',
'shadow-1xs',
'depth-flat:shadow-none',
'p-4',
'pr-8',
'bottom-4',
@@ -85,7 +83,6 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
'justify-center',
'items-center',
'rounded-sm',
'circular-corners:rounded-full',
'hover:bg-tint-hover'
)}
title={tString(language, 'close')}
@@ -10,10 +10,8 @@ import {
} from '@/components/primitives';
import type { ClassValue } from '@/lib/tailwind';
import { nullIfNever } from '@/lib/typescript';
import { BlockContentRef } from './BlockContentRef';
import { CodeBlock } from './CodeBlock';
import { Columns } from './Columns';
import { Divider } from './Divider';
import type { DocumentContextProps } from './DocumentView';
import { Drawing } from './Drawing';
@@ -46,6 +44,13 @@ export interface BlockProps<Block extends DocumentBlock> extends DocumentContext
style?: ClassValue;
}
/**
* Alternative to `assertNever` that returns `null` instead of throwing an error.
*/
function nullIfNever(_value: never): null {
return null;
}
export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
const { block, style, isEstimatedOffscreen, context } = props;
@@ -63,8 +68,6 @@ export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
return <List {...props} block={block} />;
case 'list-item':
return <ListItem {...props} block={block} />;
case 'columns':
return <Columns {...props} block={block} />;
case 'code':
return <CodeBlock {...props} block={block} />;
case 'hint':
@@ -109,7 +112,6 @@ export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
case 'image':
case 'code-line':
case 'tabs-item':
case 'column':
throw new Error(`Blocks (${block.type}) should be directly rendered by parent`);
default:
return nullIfNever(block);
@@ -166,7 +168,6 @@ export function BlockSkeleton(props: { block: DocumentBlock; style: ClassValue }
case 'integration':
case 'stepper':
case 'reusable-content':
case 'columns':
return <SkeletonCard id={id} style={style} />;
case 'embed':
case 'images':
@@ -175,7 +176,6 @@ export function BlockSkeleton(props: { block: DocumentBlock; style: ClassValue }
case 'image':
case 'code-line':
case 'tabs-item':
case 'column':
throw new Error(`Blocks (${block.type}) should be directly rendered by parent`);
default:
return nullIfNever(block);
@@ -70,8 +70,8 @@ export function UnwrappedBlocks<TBlock extends DocumentBlock>(props: UnwrappedBl
style={[
'mx-auto w-full decoration-primary/6',
node.data && 'fullWidth' in node.data && node.data.fullWidth
? 'max-w-screen-2xl'
: 'page-full-width:ml-0 max-w-3xl',
? 'max-w-screen-xl'
: 'max-w-3xl',
blockStyle,
]}
isEstimatedOffscreen={isOffscreen}
@@ -42,7 +42,7 @@ export function Caption(
'after:pointer-events-none',
fit ? 'w-fit' : null,
withBorder
? 'rounded circular-corners:rounded-2xl straight-corners:rounded-none after:border-tint-subtle after:border after:rounded circular-corners:after:rounded-2xl straight-corners:after:rounded-none dark:after:mix-blend-plus-lighter after:pointer-events-none'
? 'rounded straight-corners:rounded-none after:border-tint-subtle after:border after:rounded straight-corners:after:rounded-none dark:after:mix-blend-plus-lighter after:pointer-events-none'
: null,
],
style,
@@ -1,80 +0,0 @@
import { type ClassValue, tcls } from '@/lib/tailwind';
import type { DocumentBlockColumns, Length } from '@gitbook/api';
import type { BlockProps } from '../Block';
import { Blocks } from '../Blocks';
export function Columns(props: BlockProps<DocumentBlockColumns>) {
const { block, style, ancestorBlocks, document, context } = props;
return (
<div className={tcls('flex flex-col gap-x-8 md:flex-row', style)}>
{block.nodes.map((columnBlock) => {
const width = columnBlock.data.width;
const { className, style } = transformLengthToCSS(width);
return (
<Column key={columnBlock.key} className={className} style={style}>
<Blocks
key={columnBlock.key}
nodes={columnBlock.nodes}
document={document}
ancestorBlocks={[...ancestorBlocks, block, columnBlock]}
context={context}
blockStyle="flip-heading-hash"
style="w-full space-y-4 *:max-w-full"
/>
</Column>
);
})}
</div>
);
}
export function Column(props: {
children?: React.ReactNode;
className?: ClassValue;
style?: React.CSSProperties;
}) {
return (
<div className={tcls('flex-col', props.className)} style={props.style}>
{props.children}
</div>
);
}
function transformLengthToCSS(length: Length | undefined) {
if (!length) {
return { className: ['md:w-full'] }; // default to full width if no length is specified
}
if (typeof length === 'number') {
return { style: undefined }; // not implemented yet with non-percentage lengths
}
if (length.unit === '%') {
return {
className: [
'md:flex-shrink-0',
COLUMN_WIDTHS[Math.round(length.value * 0.01 * (COLUMN_WIDTHS.length - 1))],
],
};
}
return { style: undefined }; // not implemented yet with non-percentage lengths
}
// Tailwind CSS classes for column widths.
// The index of the array corresponds to the percentage width of the column.
const COLUMN_WIDTHS = [
'md:w-0',
'md:w-1/12',
'md:w-2/12',
'md:w-3/12',
'md:w-4/12',
'md:w-5/12',
'md:w-6/12',
'md:w-7/12',
'md:w-8/12',
'md:w-9/12',
'md:w-10/12',
'md:w-11/12',
'md:w-full',
];
@@ -1 +0,0 @@
export * from './Columns';
@@ -7,5 +7,5 @@ import type { BlockProps } from './Block';
export function Divider(props: BlockProps<DocumentBlockDivider>) {
const { style } = props;
return <hr className={tcls(style, 'page-full-width:max-w-full border-tint-subtle')} />;
return <hr className={tcls(style, 'border-tint-subtle')} />;
}
@@ -23,7 +23,6 @@ export function Hint(props: BlockProps<DocumentBlockHint>) {
'rounded-md',
hasHeading ? 'rounded-l' : null,
'straight-corners:rounded-none',
'circular-corners:rounded-xl',
'overflow-hidden',
hasHeading ? ['border-l-2', hintStyle.containerWithHeader] : hintStyle.container,
@@ -1,4 +1,9 @@
import type { DocumentBlockImage, DocumentBlockImages, JSONDocument, Length } from '@gitbook/api';
import type {
DocumentBlockImage,
DocumentBlockImageDimension,
DocumentBlockImages,
JSONDocument,
} from '@gitbook/api';
import { Image, type ImageResponsiveSize } from '@/components/utils';
import { resolveContentRef } from '@/lib/references';
@@ -114,7 +119,7 @@ async function ImageBlock(props: {
* When using relative values, the converted dimension will be relative to the parent element's size.
*/
function getImageDimension<DefaultValue>(
dimension: Length | undefined,
dimension: DocumentBlockImageDimension | undefined,
defaultValue: DefaultValue
): string | DefaultValue {
if (typeof dimension === 'number') {
@@ -1,6 +1,16 @@
import type { DocumentInline, JSONDocument } from '@gitbook/api';
import type {
DocumentInline,
DocumentInlineAnnotation,
DocumentInlineButton,
DocumentInlineEmoji,
DocumentInlineImage,
DocumentInlineLink,
DocumentInlineMath,
DocumentInlineMention,
JSONDocument,
} from '@gitbook/api';
import assertNever from 'assert-never';
import { nullIfNever } from '@/lib/typescript';
import { Annotation } from './Annotation/Annotation';
import type { DocumentContextProps } from './DocumentView';
import { Emoji } from './Emoji';
@@ -29,7 +39,16 @@ export interface InlineProps<T extends DocumentInline> extends DocumentContextPr
children?: React.ReactNode;
}
export function Inline<T extends DocumentInline>(props: InlineProps<T>) {
export function Inline<
T extends
| DocumentInlineImage
| DocumentInlineAnnotation
| DocumentInlineEmoji
| DocumentInlineLink
| DocumentInlineMath
| DocumentInlineMention
| DocumentInlineButton,
>(props: InlineProps<T>) {
const { inline, ...contextProps } = props;
switch (inline.type) {
@@ -47,11 +66,7 @@ export function Inline<T extends DocumentInline>(props: InlineProps<T>) {
return <InlineImage {...contextProps} inline={inline} />;
case 'button':
return <InlineButton {...contextProps} inline={inline} />;
case 'expression':
// The GitBook API should take care of evaluating expressions.
// We should never need to render them.
return null;
default:
return nullIfNever(inline);
assertNever(inline);
}
}
@@ -12,8 +12,7 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
const resolved = context.contentContext
? await resolveContentRef(inline.data.ref, context.contentContext, {
// We don't want to resolve the anchor text here, as it can be very expensive and will block rendering if there is a lot of anchors link.
resolveAnchorText: false,
resolveAnchorText: true,
})
: null;
@@ -271,7 +271,7 @@ body {
}
.scalar-activate-button {
@apply flex gap-2 items-center;
@apply bg-primary-solid text-contrast-primary-solid hover:bg-primary-solid-hover hover:text-contrast-primary-solid-hover contrast-more:ring-1 rounded-md straight-corners:rounded-none circular-corners:rounded-full circular-corners:px-3 place-self-start;
@apply bg-primary-solid text-contrast-primary-solid hover:bg-primary-solid-hover hover:text-contrast-primary-solid-hover contrast-more:ring-1 rounded-md straight-corners:rounded-none place-self-start;
@apply ring-1 ring-tint hover:ring-tint-hover;
@apply shadow-sm shadow-tint dark:shadow-tint-1 hover:shadow-md active:shadow-none;
@apply contrast-more:ring-tint-12 contrast-more:hover:ring-2 contrast-more:hover:ring-tint-12;
@@ -6,7 +6,7 @@
}
.openapi-schemas {
@apply flex flex-col mb-14 gap-0 flex-1;
@apply flex flex-col mb-14 flex-1;
}
.openapi-schemas-title {
@@ -310,26 +310,6 @@
@apply py-2 border-b border-tint-subtle max-w-full flex-1;
}
.openapi-securities-oauth-flows {
@apply flex flex-col gap-2 divide-y divide-tint-subtle;
}
.openapi-securities-oauth-content {
@apply prose *:!prose-sm *:text-tint;
}
.openapi-securities-oauth-content.openapi-markdown code {
@apply text-xs;
}
.openapi-securities-oauth-content ul {
@apply !my-0;
}
.openapi-securities-url {
@apply ml-0.5 px-0.5 rounded hover:bg-tint transition-colors;
}
.openapi-securities-body {
@apply flex flex-col gap-2;
}
@@ -501,7 +481,7 @@
.openapi-panel,
.openapi-codesample,
.openapi-response-examples {
@apply border rounded-md straight-corners:rounded-none circular-corners:rounded-xl bg-tint-subtle border-tint-subtle shadow-sm;
@apply border rounded-md straight-corners:rounded-none bg-tint-subtle border-tint-subtle shadow-sm;
}
.openapi-panel pre,
@@ -591,7 +571,7 @@ body:has(.openapi-select-popover) {
}
.openapi-select-popover {
@apply min-w-32 z-10 max-w-[max(20rem,var(--trigger-width))] overflow-x-hidden max-h-52 overflow-y-auto p-1.5 border border-tint-subtle bg-tint-base backdrop-blur-xl rounded-md circular-corners:rounded-xl straight-corners:rounded-none;
@apply min-w-32 z-10 max-w-[max(20rem,var(--trigger-width))] overflow-x-hidden max-h-52 overflow-y-auto p-1.5 border border-tint-subtle bg-tint-base backdrop-blur-xl rounded-md straight-corners:rounded-none;
@apply shadow-md shadow-tint-12/1 dark:shadow-tint-1/1;
}
@@ -700,7 +680,7 @@ body:has(.openapi-select-popover) {
/* Disclosure group */
.openapi-disclosure-group {
@apply border-tint-subtle transition-all border-b border-x overflow-auto last:rounded-b-md straight-corners:last:rounded-none circular-corners:last:rounded-b-xl first:rounded-t-md straight-corners:first:rounded-none circular-corners:first:rounded-t-xl first:border-t relative;
@apply border-tint-subtle transition-all border-b border-x overflow-auto last:rounded-b-md straight-corners:last:rounded-none first:rounded-t-md straight-corners:first:rounded-none first:border-t relative;
}
.openapi-disclosure-group:has(.openapi-disclosure-group-trigger:hover) {
@@ -748,12 +728,8 @@ body:has(.openapi-select-popover) {
}
/* Disclosure */
.openapi-schemas-disclosure {
@apply border-t border-x last:border-b border-tint-subtle !ring-0 first:!rounded-t-xl last:!rounded-b-xl !rounded-none;
}
.openapi-schemas-disclosure > .openapi-disclosure-trigger {
@apply flex items-center font-mono transition-all font-normal text-tint-strong !text-sm hover:bg-tint-subtle relative flex-1 gap-2.5 p-5 truncate -outline-offset-1;
@apply flex items-center font-mono transition-all text-tint-strong !text-sm hover:bg-tint-subtle relative flex-1 gap-2.5 p-3 truncate -outline-offset-1;
}
.openapi-schemas-disclosure > .openapi-disclosure-trigger,
@@ -797,6 +773,7 @@ body:has(.openapi-select-popover) {
@apply bg-tint-subtle overflow-hidden;
}
.openapi-disclosure:has(> .openapi-disclosure-trigger:hover),
.openapi-disclosure[data-expanded="true"] {
@apply ring-1 shadow-sm;
}
@@ -809,7 +786,7 @@ body:has(.openapi-select-popover) {
}
.openapi-disclosure-trigger-label {
@apply absolute right-3 px-2 h-5 justify-end shrink-0 ring-tint-subtle truncate text-tint duration-300 transition-all rounded straight-corners:rounded-none circular-corners:rounded-xl flex flex-row gap-1 items-center text-xs;
@apply absolute right-3 px-2 h-5 justify-end shrink-0 ring-tint-subtle truncate text-tint duration-300 transition-all rounded straight-corners:rounded-none flex flex-row gap-1 items-center text-xs;
}
.openapi-disclosure-trigger-label span {
@@ -871,7 +848,7 @@ body:has(.openapi-select-popover) {
}
.openapi-tooltip {
@apply flex items-center gap-1 bg-tint-base border border-tint-subtle text-tint-strong rounded-md straight-corners:rounded-none circular-corners:rounded-lg font-medium px-1.5 py-0.5 shadow-sm text-[13px];
@apply flex items-center gap-1 bg-tint-base border border-tint-subtle text-tint-strong rounded-md straight-corners:rounded-none font-medium px-1.5 py-0.5 shadow-sm text-[13px];
}
.openapi-tooltip svg {
@@ -34,13 +34,13 @@ export function StepperStep(props: BlockProps<DocumentBlockStepperStep>) {
<div className="relative select-none">
<div
className={tcls(
'can-override-bg can-override-text flex size-[calc(1.75rem+1px)] items-center justify-center rounded-full bg-primary-solid theme-muted:bg-primary-subtle tabular-nums',
'font-medium text-contrast-primary-solid theme-muted:text-primary'
'can-override-bg can-override-text flex size-[calc(1.75rem+1px)] items-center justify-center rounded-full bg-primary-subtle tabular-nums',
'font-medium text-primary'
)}
>
{index + 1}
</div>
<div className="can-override-bg absolute top-9 bottom-2 left-[0.875rem] w-px bg-primary-7 theme-muted:bg-primary-subtle" />
<div className="can-override-bg absolute top-9 bottom-2 left-[0.875rem] w-px bg-primary-subtle" />
</div>
<Blocks
{...contextProps}
@@ -50,9 +50,8 @@ export async function RecordCard(
'w-[calc(100%+2px)]',
'h-[calc(100%+2px)]',
'inset-[-1px]',
'rounded',
'rounded-[7px]',
'straight-corners:rounded-none',
'circular-corners:rounded-xl',
'overflow-hidden',
'[&_.heading>div:first-child]:hidden',
'[&_.heading>div]:text-[.8em]',
@@ -148,10 +147,8 @@ export async function RecordCard(
'grid',
'shadow-1xs',
'shadow-tint-9/1',
'depth-flat:shadow-none',
'rounded',
'rounded-md',
'straight-corners:rounded-none',
'circular-corners:rounded-xl',
'dark:shadow-transparent',
'before:pointer-events-none',
@@ -38,8 +38,7 @@ export function ViewGrid(props: TableViewProps<DocumentTableViewGrid>) {
className={tcls(
tableWidth,
styles.rowGroup,
'straight-corners:rounded-none',
'circular-corners:rounded-xl'
'straight-corners:rounded-none'
)}
>
<div role="row" className={tcls('flex', 'w-full')}>
@@ -36,16 +36,12 @@ export function Footer(props: { context: GitBookSiteContext }) {
<div className={tcls(CONTAINER_STYLE, 'px-4', 'py-8', 'lg:py-12', 'mx-auto')}>
<div
className={tcls(
'lg:!max-w-none mx-auto grid max-w-3xl site-full-width:max-w-screen-2xl justify-between gap-12',
'mx-auto grid max-w-3xl justify-between gap-12 lg:max-w-none',
'grid-cols-[auto_auto]',
'lg:grid-cols-[18rem_minmax(auto,_48rem)_auto]',
'xl:grid-cols-[18rem_minmax(auto,_48rem)_14rem]',
'site-full-width:lg:grid-cols-[18rem_minmax(auto,_80rem)_auto]',
'site-full-width:xl:grid-cols-[18rem_minmax(auto,_80rem)_14rem]',
'page-no-toc:lg:grid-cols-[minmax(auto,_48rem)_auto]',
'page-no-toc:xl:grid-cols-[14rem_minmax(auto,_48rem)_14rem]',
'[body:has(.site-full-width,.page-no-toc)_&]:lg:grid-cols-[minmax(auto,_90rem)_auto]',
'[body:has(.site-full-width,.page-no-toc)_&]:xl:grid-cols-[14rem_minmax(auto,_90rem)_14rem]'
'page-no-toc:xl:grid-cols-[14rem_minmax(auto,_48rem)_14rem]'
)}
>
{
@@ -106,7 +102,7 @@ export function Footer(props: { context: GitBookSiteContext }) {
'col-span-2 page-has-toc:lg:col-span-1 page-has-toc:lg:col-start-2 page-no-toc:xl:col-span-1 page-no-toc:xl:col-start-2'
)}
>
<div className="mx-auto flex max-w-3xl site-full-width:max-w-screen-2xl flex-col gap-10 sm:flex-row sm:gap-6">
<div className="mx-auto flex max-w-3xl flex-col gap-10 sm:flex-row sm:gap-6">
{partition(customization.footer.groups, FOOTER_COLUMNS).map(
(column, columnIndex) => (
<div
@@ -104,9 +104,11 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
'lg:basis-40',
'md:max-w-[40%]',
'lg:max-w-lg',
'lg:ml-[max(calc((100%-18rem-48rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem)
'lg:ml-[max(calc((100%-18rem-48rem-3rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem) - margin (3rem)
'xl:ml-[max(calc((100%-18rem-48rem-14rem-3rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem) - outline (14rem) - margin (3rem)
'page-no-toc:lg:ml-[max(calc((100%-18rem-48rem-18rem-3rem)/2),0rem)]',
'page-full-width:lg:ml-[max(calc((100%-18rem-103rem-3rem)/2),1.5rem)]',
'page-full-width:2xl:ml-[max(calc((100%-18rem-96rem-14rem+3rem)/2),1.5rem)]',
'md:mr-auto',
'order-last',
'md:order-[unset]',
@@ -193,6 +195,7 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
<div
className={tcls(
CONTAINER_STYLE,
'page-default-width:max-w-[unset]',
'grow',
'flex',
'items-end',
@@ -1,7 +1,8 @@
import type { SiteSpace } from '@gitbook/api';
import { getSiteSpaceURL } from '@/lib/sites';
import { tcls } from '@/lib/tailwind';
import { joinPath } from '@/lib/paths';
import type { GitBookSiteContext } from '@v2/lib/context';
import { DropdownChevron, DropdownMenu } from './DropdownMenu';
import { SpacesDropdownMenuItem } from './SpacesDropdownMenuItem';
@@ -13,6 +14,7 @@ export function SpacesDropdown(props: {
className?: string;
}) {
const { context, siteSpace, siteSpaces, className } = props;
const { linker } = context;
return (
<DropdownMenu
@@ -60,7 +62,7 @@ export function SpacesDropdown(props: {
className
)}
>
<span className={tcls('truncate', 'grow')}>{siteSpace.title}</span>
<span className={tcls('line-clamp-1', 'grow')}>{siteSpace.title}</span>
<DropdownChevron />
</div>
}
@@ -71,7 +73,9 @@ export function SpacesDropdown(props: {
variantSpace={{
id: otherSiteSpace.id,
title: otherSiteSpace.title,
url: getSiteSpaceURL(context, otherSiteSpace),
url: otherSiteSpace.urls.published
? linker.toLinkForContent(otherSiteSpace.urls.published)
: getFallbackSiteSpaceURL(otherSiteSpace, context),
}}
active={otherSiteSpace.id === siteSpace.id}
/>
@@ -79,3 +83,14 @@ export function SpacesDropdown(props: {
</DropdownMenu>
);
}
/**
* When the site is not published yet, `urls.published` is not available.
* To ensure navigation works in preview, we compute a relative URL from the siteSpace path.
*/
function getFallbackSiteSpaceURL(siteSpace: SiteSpace, context: GitBookSiteContext) {
const { linker, sections } = context;
return linker.toPathInSite(
sections?.current ? joinPath(sections.current.path, siteSpace.path) : siteSpace.path
);
}
@@ -9,6 +9,7 @@ import {
} from '@gitbook/api';
import { Icon } from '@gitbook/icons';
import type { GitBookSiteContext, GitBookSpaceContext } from '@v2/lib/context';
import { getPageDocument } from '@v2/lib/data';
import type { GitBookLinker } from '@v2/lib/links';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
@@ -28,7 +29,6 @@ import { PageControlButtons } from './PageControlButtons';
import { PrintButton } from './PrintButton';
import './pdf.css';
import { sanitizeGitBookAppURL } from '@/lib/app';
import { getPageDocument } from '@v2/lib/data';
const DEFAULT_LIMIT = 100;
@@ -224,7 +224,8 @@ async function PDFPageDocument(props: {
context: GitBookSpaceContext;
}) {
const { page, context } = props;
const document = await getPageDocument(context, page);
const { space } = context;
const document = await getPageDocument(context.dataFetcher, space, page);
return (
<PrintPage id={getPagePDFContainerId(page)}>
@@ -30,7 +30,6 @@ export function AsideSectionHighlight({
'rounded-md',
'straight-corners:rounded-none',
'circular-corners:rounded-2xl',
'sidebar-list-line:rounded-l-none',
'sidebar-list-pill:bg-primary',
@@ -48,9 +48,11 @@ export function PageAside(props: {
'group/aside',
'hidden',
'xl:flex',
// 'page-no-toc:lg:flex',
'flex-col',
'basis-56',
'xl:ml-12',
// 'page-no-toc:basis-40',
// 'page-no-toc:xl:basis-56',
'grow-0',
'shrink-0',
'break-anywhere', // To prevent long words in headings from breaking the layout
@@ -82,7 +82,6 @@ export function ScrollSectionsList(props: { sections: DocumentSection[] }) {
'rounded-md',
'straight-corners:rounded-none',
'circular-corners:rounded-2xl',
'sidebar-list-line:rounded-l-none',
'hover:bg-tint-hover',
@@ -27,9 +27,7 @@ export function PageBody(props: {
const { page, context, ancestors, document, withPageFeedback } = props;
const { customization } = context;
const contentFullWidth = document ? hasFullWidthBlock(document) : false;
const pageFullWidth = page.id === 'wtthNFMqmEQmnt5LKR0q';
const asFullWidth = pageFullWidth || contentFullWidth;
const asFullWidth = document ? hasFullWidthBlock(document) : false;
const language = getSpaceLanguage(customization);
const updatedAt = page.updatedAt ?? page.createdAt;
@@ -38,11 +36,15 @@ export function PageBody(props: {
<main
className={tcls(
'relative min-w-0 flex-1',
'mx-auto max-w-screen-2xl py-8',
'py-8 lg:px-12',
// Allow words to break if they are too long.
'break-anywhere',
pageFullWidth ? 'page-full-width 2xl:px-8' : 'page-default-width',
asFullWidth ? 'site-full-width' : 'site-default-width',
// When in api page mode without the aside, we align with the border of the main content
'page-api-block:xl:max-2xl:pr-0',
// Max size to ensure one column in api is aligned with rest of content (2 x 3xl) + (gap-3 + 2) * px-12
'page-api-block:mx-auto page-api-block:max-w-screen-2xl',
// page.layout.tableOfContents ? null : 'xl:ml-56',
asFullWidth ? 'page-full-width' : 'page-default-width',
page.layout.tableOfContents ? 'page-has-toc' : 'page-no-toc'
)}
>
@@ -79,7 +81,7 @@ export function PageBody(props: {
<PageFooterNavigation context={context} page={page} />
) : null}
<div className="mx-auto mt-6 page-api-block:ml-0 flex max-w-3xl page-full-width:max-w-screen-2xl flex-row flex-wrap items-center gap-4 text-tint contrast-more:text-tint-strong">
<div className="mx-auto mt-6 page-api-block:ml-0 flex max-w-3xl flex-row flex-wrap items-center gap-4 text-tint contrast-more:text-tint-strong">
{updatedAt ? (
<p className="mr-auto text-sm">
{t(language, 'page_last_modified', <DateRelative value={updatedAt} />)}
@@ -22,7 +22,6 @@ export async function PageCover(props: {
}) {
const { as, page, cover, context } = props;
const resolved = cover.ref ? await resolveContentRef(cover.ref, context) : null;
const resolvedDark = cover.refDark ? await resolveContentRef(cover.refDark, context) : null;
return (
<div
@@ -34,20 +33,14 @@ export async function PageCover(props: {
? [
'sm:-mx-6',
'md:-mx-8',
'lg:-mr-8',
'lg:-ml-12',
'-lg:mr-8',
'lg:ml-0',
!page.layout.tableOfContents &&
context.customization.header.preset !== 'none'
? 'xl:-ml-[19rem]'
? 'lg:-ml-64'
: null,
]
: [
'sm:mx-auto',
'max-w-3xl ',
'page-full-width:max-w-screen-2xl',
'sm:rounded-md',
'mb-8',
]
: ['sm:mx-auto', 'max-w-3xl', 'sm:rounded-md', 'mb-8']
)}
>
<Image
@@ -65,12 +58,6 @@ export async function PageCover(props: {
height: defaultPageCover.height,
},
},
dark: resolvedDark
? {
src: resolvedDark.href,
size: resolvedDark.file?.dimensions,
}
: null,
}}
resize={
// When using the default cover, we don't want to resize as it's a SVG
@@ -32,8 +32,8 @@ export async function PageFooterNavigation(props: {
'mt-6',
'gap-2',
'max-w-3xl',
'page-full-width:max-w-screen-2xl',
'mx-auto',
'page-api-block:ml-0',
'text-tint'
)}
>
@@ -107,7 +107,6 @@ function NavigationCard(
'border',
'border-tint-subtle',
'rounded',
'circular-corners:rounded-2xl',
'straight-corners:rounded-none',
'hover:border-primary',
'text-pretty',
@@ -23,14 +23,7 @@ export async function PageHeader(props: {
return (
<header
className={tcls(
'max-w-3xl',
'page-full-width:max-w-screen-2xl',
'mx-auto',
'mb-6',
'space-y-3',
'page-api-block:ml-0'
)}
className={tcls('max-w-3xl', 'mx-auto', 'mb-6', 'space-y-3', 'page-api-block:ml-0')}
>
{ancestors.length > 0 && (
<nav>
@@ -3,12 +3,12 @@ import * as React from 'react';
/**
* This component preserves the layout of the page while loading a new one.
* This approach is needed as page layout (full width block) is done using CSS (`body:has(.full-width)`),
* This approach is needed as page layout (full width block) is done using CSS (`body:has(.page-full-width)`),
* which becomes false while transitioning between the 2 page states:
*
* 1. Page 1 with full width block: `body:has(.site-full-width)` is true
* 2. Loading skeleton while transitioning to page 2: `body:has(.site-full-width)` is false
* 3. Page 2 with full width block: `body:has(.site-full-width)` is true
* 1. Page 1 with full width block: `body:has(.page-full-width)` is true
* 2. Loading skeleton while transitioning to page 2: `body:has(.page-full-width)` is false
* 3. Page 2 with full width block: `body:has(.page-full-width)` is true
*
* This component ensures that the layout is preserved while transitioning between the 2 page states (in step 2).
*/
@@ -24,9 +24,9 @@ export function PreservePageLayout(props: { asFullWidth: boolean }) {
}
if (asFullWidth) {
header.classList.add('site-full-width');
header.classList.add('page-full-width');
} else {
header.classList.remove('site-full-width');
header.classList.remove('page-full-width');
}
}, [asFullWidth]);
@@ -64,10 +64,10 @@ export function PageFeedbackForm(props: {
<div className="rounded-full border border-tint-subtle bg-tint-base contrast-more:border-tint-12">
<div className="flex">
<RatingButton
rating={PageFeedbackRating.Good}
label={tString(languages, 'was_this_helpful_positive')}
onClick={() => onSubmitRating(PageFeedbackRating.Good)}
active={rating === PageFeedbackRating.Good}
rating={PageFeedbackRating.Bad}
label={tString(languages, 'was_this_helpful_negative')}
onClick={() => onSubmitRating(PageFeedbackRating.Bad)}
active={rating === PageFeedbackRating.Bad}
disabled={rating !== undefined}
/>
<RatingButton
@@ -78,10 +78,10 @@ export function PageFeedbackForm(props: {
disabled={rating !== undefined}
/>
<RatingButton
rating={PageFeedbackRating.Bad}
label={tString(languages, 'was_this_helpful_negative')}
onClick={() => onSubmitRating(PageFeedbackRating.Bad)}
active={rating === PageFeedbackRating.Bad}
rating={PageFeedbackRating.Good}
label={tString(languages, 'was_this_helpful_positive')}
onClick={() => onSubmitRating(PageFeedbackRating.Good)}
active={rating === PageFeedbackRating.Good}
disabled={rating !== undefined}
/>
</div>
@@ -1,4 +1,5 @@
import {
CustomizationCorners,
CustomizationHeaderPreset,
CustomizationIconsStyle,
CustomizationSidebarBackgroundStyle,
@@ -76,13 +77,14 @@ export async function CustomizationRootLayout(props: {
customization.header.preset === CustomizationHeaderPreset.None
? null
: 'scroll-pt-[76px]', // Take the sticky header in consideration for the scrolling
customization.styling.corners && `${customization.styling.corners}-corners`,
customization.styling.corners === CustomizationCorners.Straight
? ' straight-corners'
: '',
'theme' in customization.styling && `theme-${customization.styling.theme}`,
tintColor ? ' tint' : 'no-tint',
sidebarStyles.background && `sidebar-${sidebarStyles.background}`,
sidebarStyles.list && `sidebar-list-${sidebarStyles.list}`,
'links' in customization.styling && `links-${customization.styling.links}`,
'depth' in customization.styling && `depth-${customization.styling.depth}`,
fontNotoColorEmoji.variable,
ibmPlexMono.variable,
fontData.type === 'default' ? fontData.variable : 'font-custom',
@@ -39,7 +39,7 @@
/* Light mode */
::-webkit-scrollbar {
@apply bg-tint-subtle;
@apply bg-tint-subtle z-50;
width: 8px;
height: 8px;
}
@@ -143,6 +143,10 @@
margin-right: 0;
width: calc(100% - var(--scrollbar-width));
}
body:has(.page-full-width) .scroll-nojump {
margin-left: 0;
width: 100%;
}
}
.elevate-link {
@@ -18,8 +18,7 @@ export function HighlightQuery(props: {
'text-bold',
'bg-primary',
'text-contrast-primary',
'px-0.5',
'-mx-0.5',
'px-1',
'py-0.5',
'rounded',
'straight-corners:rounded-sm',
@@ -1,21 +1,19 @@
'use client';
import { Icon } from '@gitbook/icons';
import { readStreamableValue } from 'ai/rsc';
import React from 'react';
import { Loading } from '@/components/primitives';
import { useLanguage } from '@/intl/client';
import { t } from '@/intl/translate';
import type { TranslationLanguage } from '@/intl/translations';
import { tcls } from '@/lib/tailwind';
import { Icon } from '@gitbook/icons';
import { readStreamableValue } from 'ai/rsc';
import React from 'react';
import { motion } from 'framer-motion';
import { useTrackEvent } from '../Insights';
import { Link } from '../primitives';
import { useSearchAskContext } from './SearchAskContext';
import { type AskAnswerResult, type AskAnswerSource, streamAskQuestion } from './server-actions';
import { useSearch, useSearchLink } from './useSearch';
export type SearchAskState =
| {
type: 'answer';
@@ -88,13 +86,22 @@ export function SearchAskAnswer(props: { query: string }) {
}, [setAskState]);
const loading = (
<div className={tcls('w-full', 'flex', 'items-center', 'justify-center')}>
<Loading className={tcls('w-6', 'py-8', 'text-primary-subtle')} />
<div key="loading" className={tcls('flex', 'flex-wrap', 'gap-2')}>
{[...Array(9)].map((_, index) => (
<div
key={index}
className="h-4 animate-[fadeIn_0.5s_ease-in-out_both,pulse_2s_ease-in-out_infinite] rounded straight-corners:rounded-none bg-tint-active"
style={{
animationDelay: `${index * 0.1}s,${0.5 + index * 0.1}s`,
width: `${((index % 5) + 1) * 15}%`,
}}
/>
))}
</div>
);
return (
<div className={tcls('max-h-[60vh]', 'overflow-y-auto')}>
<motion.div className={tcls('mx-auto w-full max-w-prose')} layout="position">
{askState?.type === 'answer' ? (
<React.Suspense fallback={loading}>
<TransitionAnswerBody answer={askState.answer} placeholder={loading} />
@@ -104,7 +111,7 @@ export function SearchAskAnswer(props: { query: string }) {
<div className={tcls('p-4')}>{t(language, 'search_ask_error')}</div>
) : null}
{askState?.type === 'loading' ? loading : null}
</div>
</motion.div>
);
}
@@ -138,10 +145,7 @@ function AnswerBody(props: { answer: AskAnswerResult }) {
return (
<>
<div
data-testid="search-ask-answer"
className={tcls('my-4', 'sm:mt-6', 'px-4', 'sm:px-12', 'text-tint-strong')}
>
<div data-testid="search-ask-answer" className={tcls('text-tint-strong')}>
{answer.body ?? t(language, 'search_ask_no_answer')}
{answer.followupQuestions.length > 0 ? (
<AnswerFollowupQuestions followupQuestions={answer.followupQuestions} />
@@ -176,14 +180,12 @@ function AnswerFollowupQuestions(props: { followupQuestions: string[] }) {
'py-2',
'rounded',
'straight-corners:rounded-none',
'circular-corners:rounded-full',
'text-tint',
'hover:bg-tint-hover',
'focus-within:bg-tint-hover'
)}
{...getSearchLinkProps({
query: question,
ask: true,
})}
>
<Icon
@@ -21,7 +21,7 @@ export function SearchButton(props: { children?: React.ReactNode; style?: ClassV
const onClick = () => {
setSearchState({
ask: false,
mode: 'both',
global: false,
query: '',
});
@@ -45,25 +45,21 @@ export function SearchButton(props: { children?: React.ReactNode; style?: ClassV
'w-full',
'py-2',
'px-3',
'circular-corners:px-4',
'gap-2',
'bg-tint-base',
'ring-1',
'ring-tint-12/2',
'depth-flat:ring-tint-subtle',
'shadow-sm',
'shadow-tint-12/3',
'dark:shadow-none',
'depth-flat:shadow-none',
'text-tint',
'rounded-lg',
'straight-corners:rounded-sm',
'circular-corners:rounded-full',
'contrast-more:ring-tint-12',
'contrast-more:text-tint-strong',
@@ -72,12 +68,10 @@ export function SearchButton(props: { children?: React.ReactNode; style?: ClassV
'hover:bg-tint-subtle',
'hover:shadow-md',
'hover:scale-102',
'depth-flat:hover:scale-100',
'hover:ring-tint-hover',
'hover:text-tint-strong',
'focus:shadow-md',
'focus:scale-102',
'depth-flat:focus:scale-100',
'focus:ring-tint-hover',
'focus:text-tint-strong',
@@ -105,7 +99,7 @@ export function SearchButton(props: { children?: React.ReactNode; style?: ClassV
);
}
function Shortcut() {
export function Shortcut() {
const [operatingSystem, setOperatingSystem] = useState<string | null>(null);
useEffect(() => {
@@ -0,0 +1,495 @@
'use client';
import { useLanguage } from '@/intl/client';
import { t } from '@/intl/translate';
import { tcls } from '@/lib/tailwind';
import { filterOutNullable } from '@/lib/typescript';
import { Icon } from '@gitbook/icons';
import { useEffect, useRef, useState } from 'react';
import { useVisitedPages } from '../Insights/useVisitedPages';
import { Button } from '../primitives';
import { Shortcut } from './SearchButton';
import { isQuestion } from './isQuestion';
import { streamAISearchAnswer, streamAISearchSummary } from './server-actions';
import { useSearch } from './useSearch';
// Types
type Message = {
role: 'assistant' | 'user';
content?: string;
context?: string;
fetching?: boolean;
};
// Loading animation component
function LoadingAnimation() {
return (
<div className="mt-2 flex flex-wrap gap-2">
{[...Array(9)].map((_, index) => (
<div
key={index}
className="h-4 animate-[fadeIn_0.5s_ease-in-out_both,pulse_2s_ease-in-out_infinite] rounded straight-corners:rounded-none bg-tint-active"
style={{
animationDelay: `${index * 0.1}s,${0.5 + index * 0.1}s`,
width: `${((index % 5) + 1) * 15}%`,
}}
/>
))}
</div>
);
}
// Followup questions component
function FollowupQuestions({
questions,
onQuestionClick,
}: {
questions: string[];
onQuestionClick: (question: string) => void;
}) {
if (!questions || questions.length === 0) return null;
return (
<div className="mx-auto flex w-full max-w-prose flex-col">
{questions.map((question) => (
<button
type="button"
key={question}
className="-mx-4 flex items-center gap-4 rounded straight-corners:rounded-none px-4 py-2 text-tint hover:bg-tint-hover"
onClick={() => onQuestionClick(question)}
>
<Icon icon="search" className="size-4" /> {question}
</button>
))}
</div>
);
}
// Individual chat message component
function ChatMessage({
message,
}: {
message: Message;
}) {
const language = useLanguage();
const isUser = message.role === 'user';
return (
<div className={tcls('flex-col gap-1', isUser && 'items-end gap-1 self-end')}>
<h5 className="flex items-center gap-1 font-semibold text-tint-subtle text-xs">
{isUser ? (
(message.context ??
`You asked ${isQuestion(message.content ?? '') ? '' : 'about'}`)
) : (
<>
<Icon icon="sparkle" className="mt-0.5 size-3" />
{message.context ?? 'AI Answer'}
</>
)}
</h5>
{message.fetching ? (
<LoadingAnimation />
) : !message.content ? (
<div className="text-tint-subtle italic">{t(language, 'search_ask_no_answer')}</div>
) : (
<div className={tcls(isUser && 'rounded-lg bg-tint-active px-4 py-2')}>
{message.content}
</div>
)}
</div>
);
}
// Chat input component
function ChatInput({
onSendMessage,
disabled,
inputRef,
}: {
onSendMessage: (message: string) => void;
disabled: boolean;
inputRef?: React.RefObject<HTMLInputElement>;
}) {
const [inputValue, setInputValue] = useState('');
const handleSend = () => {
if (!inputValue.trim()) return;
onSendMessage(inputValue);
setInputValue('');
};
return (
<div className="flex gap-2">
<div className="relative flex grow">
<input
ref={inputRef}
type="text"
placeholder="Ask a follow-up question"
className="grow rounded px-4 py-1 ring-1 ring-tint-subtle"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
disabled={disabled}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
}}
/>
{!disabled && (
<div className="-translate-y-1/2 absolute top-1/2 right-2.5">
<Shortcut />
</div>
)}
</div>
<Button
label="Send"
iconOnly
icon="arrow-up"
size="medium"
className="shrink-0"
onClick={handleSend}
/>
</div>
);
}
// Custom hook for AI streaming
function useAIStream({
question,
previousResponseId,
}: {
question?: string;
previousResponseId?: string;
}) {
const [response, setResponse] = useState<{
content?: React.ReactNode;
responseId?: string;
followupQuestions?: string[];
fetching: boolean;
}>({
fetching: false,
});
useEffect(() => {
if (!question) return;
let cancelled = false;
setResponse({ fetching: true });
(async () => {
try {
const stream = await streamAISearchAnswer({
question,
previousResponseId,
});
for await (const data of stream) {
if (cancelled) break;
if (!data) continue;
setResponse((prev) => {
const updated = { ...prev, fetching: false };
if (data.responseId) {
updated.responseId = String(data.responseId);
}
if (data.answer) {
updated.content = data.answer;
}
if (data.followupQuestions) {
updated.followupQuestions = data.followupQuestions;
}
return updated;
});
}
} catch (error) {
console.error('Error in AI stream:', error);
setResponse((prev) => ({ ...prev, fetching: false }));
}
})();
return () => {
cancelled = true;
};
}, [question, previousResponseId]);
return response;
}
// Summary hook
function useSummary(visitedPages: any[]) {
const [summary, setSummary] = useState<React.ReactNode | undefined>(undefined);
const [summaryResponseId, setSummaryResponseId] = useState<string | undefined>(undefined);
useEffect(() => {
let cancelled = false;
(async () => {
try {
const stream = await streamAISearchSummary({ visitedPages });
for await (const data of stream) {
if (cancelled) break;
if (!data) continue;
if (data.responseId) {
setSummaryResponseId(String(data.responseId));
}
if (data.output) {
setSummary(data.output);
}
}
} catch (error) {
console.error('Error in summary stream:', error);
}
})();
return () => {
cancelled = true;
};
}, [visitedPages]);
return { summary, summaryResponseId };
}
// Main component
export function SearchChat(props: {
query: string;
chatInputRef?: React.RefObject<HTMLInputElement>;
}) {
const { query, chatInputRef } = props;
const visitedPages = useVisitedPages((state) => state.pages);
const [messages, setMessages] = useState<Message[]>([]);
const [followupQuestions, setFollowupQuestions] = useState<string[]>([]);
const [conversationResponseId, setConversationResponseId] = useState<string | undefined>();
const [searchState, setSearchState] = useSearch();
const latestMessageRef = useRef<HTMLDivElement>(null);
const isExpanded = searchState?.mode === 'chat';
// Get summary of visited pages
const { summary, summaryResponseId } = useSummary(visitedPages);
// Handle initial query
const initialResponse = useAIStream({
question: query,
previousResponseId: summaryResponseId,
});
// Set up initial query effect
useEffect(() => {
if (!query) return;
// Add initial assistant message
setMessages([
{
role: 'assistant',
context: `You asked ${isQuestion(query) ? '' : 'about'} "${query}"`,
fetching: true,
},
]);
setFollowupQuestions([]);
setConversationResponseId(undefined);
}, [query]);
// Update message when initial response changes
useEffect(() => {
if (!query || !initialResponse) return;
if (initialResponse.content !== undefined) {
setMessages([
{
role: 'assistant',
context: `You asked ${isQuestion(query) ? '' : 'about'} "${query}"`,
content: initialResponse.content,
fetching: initialResponse.fetching,
},
]);
}
if (initialResponse.followupQuestions) {
setFollowupQuestions(initialResponse.followupQuestions);
}
if (initialResponse.responseId) {
setConversationResponseId(initialResponse.responseId);
}
}, [initialResponse, query]);
// Handle follow-up messages
const handleSendMessage = (message: string) => {
// Add user message
const newMessages: Message[] = [
...messages,
{ role: 'user', content: message, fetching: false },
{ role: 'assistant', fetching: true },
];
setMessages(newMessages);
setFollowupQuestions([]);
if (!searchState?.manual) {
setSearchState((state) => (state ? { ...state, mode: 'chat' } : null));
}
// Get AI response
const cancelled = false;
(async () => {
try {
const stream = await streamAISearchAnswer({
question: message,
previousResponseId: conversationResponseId,
});
for await (const rawData of stream) {
if (cancelled) break;
if (!rawData) continue;
// Use type assertion
const data = rawData as any;
if (data.responseId) {
setConversationResponseId(String(data.responseId));
}
if (data.answer !== undefined) {
setMessages((prev) => [
...prev.slice(0, -1),
{ role: 'assistant', content: data.answer, fetching: false },
]);
}
if (data.followupQuestions && Array.isArray(data.followupQuestions)) {
setFollowupQuestions(data.followupQuestions.filter(filterOutNullable));
}
}
} catch (error) {
console.error('Error in follow-up stream:', error);
// Update the message to show an error state
setMessages((prev) => [
...prev.slice(0, -1),
{ role: 'assistant', fetching: false },
]);
}
})();
};
// Handle followup question click
const handleFollowupClick = (question: string) => {
handleSendMessage(question);
};
// Auto-scroll to latest message
useEffect(() => {
if (latestMessageRef.current) {
latestMessageRef.current.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
}, [messages]);
return (
<div className="flex h-full flex-col overflow-hidden">
{/* Toggle button for showing search results */}
{searchState?.mode === 'chat' && (
<div
className="absolute top-2 animate-fadeIn max-md:right-4 md:top-4 md:left-4"
style={{ animationDelay: '500ms' }}
>
<Button
label="Show search results"
variant="secondary"
size="small"
iconOnly
icon="arrow-down-from-line"
className="md:hidden"
onClick={() => {
setSearchState((state) =>
state ? { ...state, mode: 'both', manual: true } : null
);
}}
/>
<Button
label="Show search results"
iconOnly
variant="blank"
size="default"
icon="sidebar"
className="hidden px-2 md:block"
onClick={() => {
setSearchState((state) =>
state ? { ...state, mode: 'both', manual: true } : null
);
}}
/>
</div>
)}
{/* Main chat area */}
<div
className={tcls(
'mx-auto flex w-full grow scroll-pt-8 flex-col gap-4 overflow-y-auto p-8 transition-all delay-200 duration-500',
isExpanded && 'md:px-16'
)}
ref={latestMessageRef}
>
{/* Summary section */}
<div className="mx-auto w-full max-w-prose">
<h5 className="mb-1 flex items-center gap-1 font-semibold text-tint-subtle text-xs">
<Icon icon="glasses-round" className="mt-0.5 size-3" /> Summary of what
you've read
</h5>
{summary ? summary : <LoadingAnimation />}
</div>
{/* Messages */}
{messages.map((message, index) => {
const isLast = index === messages.length - 1;
return (
<div
key={index}
ref={isLast ? latestMessageRef : undefined}
className={tcls(
'mx-auto flex flex w-full max-w-prose flex-col gap-4',
isLast && 'min-h-[calc(100%-2rem)]'
)}
>
<ChatMessage message={message} />
{isLast && followupQuestions && followupQuestions.length > 0 && (
<FollowupQuestions
questions={followupQuestions}
onQuestionClick={handleFollowupClick}
/>
)}
</div>
);
})}
</div>
{/* Input area */}
{query && (
<div
className={tcls(
'border-tint-subtle border-t bg-tint-subtle px-8 py-4 transition-all delay-200 duration-500',
isExpanded && 'md:px-16'
)}
>
<div className={tcls('mx-auto flex w-full max-w-prose flex-col gap-2')}>
<ChatInput
onSendMessage={handleSendMessage}
disabled={!conversationResponseId}
inputRef={chatInputRef}
/>
</div>
</div>
)}
</div>
);
}
@@ -1,6 +1,4 @@
'use client';
import { Icon } from '@gitbook/icons';
import { AnimatePresence, motion } from 'framer-motion';
import { useRouter } from 'next/navigation';
import React from 'react';
@@ -8,10 +6,9 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
import { LoadingPane } from '../primitives/LoadingPane';
import { SearchAskAnswer } from './SearchAskAnswer';
import { SearchAskProvider, useSearchAskState } from './SearchAskContext';
import { SearchChat } from './SearchChat';
import { SearchResults, type SearchResultsRef } from './SearchResults';
import { SearchScopeToggle } from './SearchScopeToggle';
import { type SearchState, type UpdateSearchState, useSearch } from './useSearch';
@@ -30,14 +27,21 @@ export function SearchModal(props: SearchModalProps) {
const searchAsk = useSearchAskState();
const [askState] = searchAsk;
const router = useRouter();
const chatInputRef = React.useRef<HTMLInputElement>(null);
useHotkeys(
'mod+k',
(e) => {
e.preventDefault();
setSearchState({ ask: false, query: '', global: false });
if (state !== null) {
// If search is already open, focus the chat input
chatInputRef.current?.focus();
} else {
// Otherwise open the search modal
setSearchState({ mode: 'both', query: '', global: false });
}
},
[]
[state]
);
// Add a global class on the body when the search modal is open
@@ -125,6 +129,7 @@ export function SearchModal(props: SearchModalProps) {
state={state}
setSearchState={setSearchState}
onClose={onClose}
chatInputRef={chatInputRef}
/>
</div>
</motion.div>
@@ -139,9 +144,11 @@ function SearchModalBody(
state: SearchState;
setSearchState: UpdateSearchState;
onClose: (to?: string) => void;
chatInputRef: React.RefObject<HTMLInputElement>;
}
) {
const { spaceTitle, withAsk, isMultiVariants, state, setSearchState, onClose } = props;
const { spaceTitle, withAsk, isMultiVariants, state, setSearchState, onClose, chatInputRef } =
props;
const language = useLanguage();
const resultsRef = React.useRef<SearchResultsRef>(null);
@@ -165,6 +172,12 @@ function SearchModalBody(
}, [onClose]);
const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
// Handle second Cmd+K
if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
event.preventDefault();
chatInputRef.current?.focus();
return;
}
if (event.key === 'ArrowUp') {
event.preventDefault();
resultsRef.current?.moveUp();
@@ -179,7 +192,7 @@ function SearchModalBody(
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchState({
ask: false, // When typing, we go back to the default search mode
mode: 'both', // When typing, we go back to the default search mode
query: event.target.value,
global: state.global,
});
@@ -219,17 +232,16 @@ function SearchModalBody(
'flex',
'flex-col',
'bg-tint-base',
'max-w-prose',
'max-w-screen-lg',
'mx-auto',
'max-h-[70dvh]',
// 'min-h-[50dvh]',
'h-[70dvh]',
'w-full',
'rounded-lg',
'straight-corners:rounded-sm',
'circular-corners:rounded-2xl',
'ring-1',
'ring-tint',
'ring-tint-hover',
'shadow-2xl',
'depth-flat:shadow-none',
'overflow-hidden',
'dark:ring-inset',
'dark:ring-tint'
@@ -244,12 +256,10 @@ function SearchModalBody(
'flex-row',
'items-start',
state.query !== null ? 'border-b' : null,
'border-tint-subtle'
'border-tint-subtle',
'col-span-full'
)}
>
<div className={tcls('p-2', 'pl-4', 'pt-4')}>
<Icon icon="magnifying-glass" className={tcls('size-4', 'text-tint-subtle')} />
</div>
<div
className={tcls(
'w-full',
@@ -272,8 +282,8 @@ function SearchModalBody(
'flex',
'resize-none',
'flex-1',
'h-12',
'p-2',
'py-4',
'px-8',
'focus:outline-none',
'bg-transparent',
'whitespace-pre-line'
@@ -289,18 +299,38 @@ function SearchModalBody(
{isMultiVariants ? <SearchScopeToggle spaceTitle={spaceTitle} /> : null}
</div>
</div>
{!state.ask || !withAsk ? (
<SearchResults
ref={resultsRef}
global={isMultiVariants && state.global}
query={normalizedQuery}
withAsk={withAsk}
onSwitchToAsk={onSwitchToAsk}
/>
) : null}
{normalizedQuery && state.ask && withAsk ? (
<SearchAskAnswer query={normalizedQuery} />
) : null}
<div className={tcls('flex grow flex-col overflow-hidden md:flex-row')}>
<div
key="results"
className={tcls(
'h-full w-full flex-1 overflow-y-auto transition-all duration-500 ease-[cubic-bezier(0.85,0,0.15,1)] *:transition-opacity *:delay-200 *:duration-300',
state.mode === 'chat' && 'flex-[0] delay-200 *:opacity-0 *:delay-0'
)}
aria-hidden={state.mode === 'chat' ? 'true' : undefined}
>
<SearchResults
ref={resultsRef}
global={isMultiVariants && state.global}
query={normalizedQuery}
withAsk={withAsk}
onSwitchToAsk={onSwitchToAsk}
/>
</div>
<div
key="chat"
className={tcls(
'relative h-full w-full flex-1 overflow-y-auto overflow-x-hidden bg-tint-subtle transition-colors duration-500 *:transition-opacity *:delay-200 *:duration-300 max-md:border-t md:border-l',
state.mode === 'results' && 'flex-[0] *:opacity-0 *:delay-0',
state.mode === 'both'
? 'border-tint-subtle'
: 'border-transparent delay-500'
)}
aria-hidden={state.mode === 'results' ? 'true' : undefined}
>
<SearchChat query={normalizedQuery} chatInputRef={chatInputRef} />
</div>
</div>
</motion.div>
);
}
@@ -2,7 +2,9 @@ import { tcls } from '@/lib/tailwind';
import { Icon, type IconName } from '@gitbook/icons';
import React from 'react';
import { Link } from '../primitives';
import { useLanguage } from '@/intl/client';
import { tString } from '@/intl/translate';
import { Button, Link } from '../primitives';
import { HighlightQuery } from './HighlightQuery';
import type { ComputedPageResult } from './server-actions';
@@ -14,6 +16,7 @@ export const SearchPageResultItem = React.forwardRef(function SearchPageResultIt
},
ref: React.Ref<HTMLAnchorElement>
) {
const language = useLanguage();
const { query, item, active } = props;
const breadcrumbs =
@@ -34,16 +37,19 @@ export const SearchPageResultItem = React.forwardRef(function SearchPageResultIt
'flex-row',
'items-center',
'p-4',
'border-t',
'border-tint-subtle',
'first:border-none',
'rounded-lg',
'straight-corners:rounded-none',
'text-base',
'font-medium',
'text-tint-strong',
'hover:bg-tint-hover',
'group',
active
? ['is-active', 'bg-primary', 'text-contrast-primary', 'hover:bg-primary-hover']
: null
active && [
'is-active',
'bg-primary',
'text-primary-strong',
'hover:bg-primary-hover',
]
)}
insights={{
type: 'search_open_result',
@@ -56,8 +62,8 @@ export const SearchPageResultItem = React.forwardRef(function SearchPageResultIt
>
<div className="size-4">
<Icon
icon="file-lines"
className={tcls('size-4', active ? 'text-primary' : 'text-tint-subtle')}
icon="file"
className={tcls('size-4', active ? 'text-primary-subtle' : 'text-tint-subtle')}
/>
</div>
<div className={tcls('flex', 'flex-col', 'w-full')}>
@@ -65,7 +71,8 @@ export const SearchPageResultItem = React.forwardRef(function SearchPageResultIt
<div
className={tcls(
'text-xs',
'opacity-6',
active ? 'text-primary-subtle' : 'text-neutral-subtle',
// 'opacity-6',
'contrast-more:opacity-11',
'font-normal',
'uppercase',
@@ -103,20 +110,15 @@ export const SearchPageResultItem = React.forwardRef(function SearchPageResultIt
) : null}
<HighlightQuery query={query} text={item.title} />
</div>
<div
className={tcls(
'p-2',
'rounded',
'straight-corners:rounded-none',
'circular-corners:rounded-full',
active ? ['bg-primary-solid', 'text-contrast-primary-solid'] : ['opacity-6']
)}
>
<Icon
icon={active ? 'arrow-turn-down-left' : 'chevron-right'}
className={tcls('size-4')}
{active ? (
<Button
icon="arrow-turn-down-left"
size="small"
label={tString(language, 'view')}
/>
</div>
) : (
<Icon icon="chevron-right" className="size-4 text-tint-subtle/6" />
)}
</Link>
);
});
@@ -1,10 +1,10 @@
import { Icon } from '@gitbook/icons';
import React from 'react';
import { t, useLanguage } from '@/intl/client';
import { t, tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
import { Link } from '../primitives';
import { Button, Link } from '../primitives';
import { useSearchLink } from './useSearch';
export const SearchQuestionResultItem = React.forwardRef(function SearchQuestionResultItem(
@@ -28,19 +28,20 @@ export const SearchQuestionResultItem = React.forwardRef(function SearchQuestion
className={tcls(
'flex',
'px-4',
recommended ? ['py-2', 'text-tint'] : 'py-4',
'py-2',
'text-tint',
'rounded-lg',
'straight-corners:rounded-none',
'hover:bg-tint-hover',
'first:mt-0',
'last:pb-3',
'gap-4',
active && [
'is-active',
'bg-primary',
'text-contrast-primary',
'text-primary-strong',
'hover:bg-primary-hover',
]
)}
{...getLinkProp({
ask: true,
query: question,
})}
>
@@ -50,7 +51,6 @@ export const SearchQuestionResultItem = React.forwardRef(function SearchQuestion
'size-4',
'shrink-0',
'mt-1.5',
'mr-4',
active ? ['text-primary'] : ['text-tint-subtle']
)}
/>
@@ -66,20 +66,16 @@ export const SearchQuestionResultItem = React.forwardRef(function SearchQuestion
</>
)}
</div>
<div
className={tcls(
'p-2',
'rounded',
'self-center',
'straight-corners:rounded-none',
'circular-corners:rounded-full',
active ? ['bg-primary-solid', 'text-contrast-primary-solid'] : ['opacity-6']
<div className="self-center">
{active ? (
<Button
icon="arrow-turn-down-left"
size="small"
label={tString(language, 'search')}
/>
) : (
<Icon icon="chevron-right" className="size-4 text-tint-subtle/6" />
)}
>
<Icon
icon={active ? 'arrow-turn-down-left' : 'chevron-right'}
className={tcls('size-4')}
/>
</div>
</Link>
);
@@ -7,6 +7,7 @@ import React from 'react';
import { t, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
import { AnimatePresence, motion } from 'framer-motion';
import { useTrackEvent } from '../Insights';
import { Loading } from '../primitives';
import { SearchPageResultItem } from './SearchPageResultItem';
@@ -18,6 +19,7 @@ import {
searchSiteSpaceContent,
streamRecommendedQuestions,
} from './server-actions';
import { type SearchState, useSearch } from './useSearch';
export interface SearchResultsRef {
moveUp(): void;
@@ -44,7 +46,6 @@ let cachedRecommendedQuestions: null | ResultType[] = null;
*/
export const SearchResults = React.forwardRef(function SearchResults(
props: {
children?: React.ReactNode;
query: string;
global: boolean;
withAsk: boolean;
@@ -52,7 +53,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
},
ref: React.Ref<SearchResultsRef>
) {
const { children, query, withAsk, global, onSwitchToAsk } = props;
const { query, withAsk, global, onSwitchToAsk } = props;
const language = useLanguage();
const trackEvent = useTrackEvent();
@@ -62,6 +63,39 @@ export const SearchResults = React.forwardRef(function SearchResults(
}>({ results: [], fetching: true });
const [cursor, setCursor] = React.useState<number | null>(null);
const refs = React.useRef<(null | HTMLAnchorElement)[]>([]);
const [searchState, setSearchState] = useSearch();
const manualStateRef = React.useRef(false);
React.useEffect(() => {
if (searchState?.manual !== undefined) {
manualStateRef.current = searchState.manual;
}
}, [searchState?.manual]);
const results: ResultType[] = React.useMemo(() => resultsState.results, [resultsState.results]);
React.useEffect(() => {
if (!query) {
// Reset the cursor when there's no query
setCursor(null);
} else if (!searchState?.manual && !resultsState.fetching && results.length === 0) {
setSearchState((prev) => {
const newState: SearchState | null = prev
? { ...prev, mode: 'chat' as const }
: null;
return newState;
});
} else if (results.length > 0) {
// Auto-focus the first result
setSearchState((prev) => {
const newState: SearchState | null = prev
? { ...prev, mode: 'both' as const }
: null;
return newState;
});
setCursor(0);
}
}, [results, query, setSearchState, resultsState.fetching, searchState?.manual]);
React.useEffect(() => {
if (!query) {
@@ -78,7 +112,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
let cancelled = false;
// Silently fetch the recommended questions, instead of showing a spinner
setResultsState({ results: [], fetching: false });
// setResultsState({ results: [], fetching: false });
// We currently have a bug where the same question can be returned multiple times.
// This is a workaround to avoid that.
@@ -150,23 +184,6 @@ export const SearchResults = React.forwardRef(function SearchResults(
};
}, [query, global, withAsk, trackEvent]);
const results: ResultType[] = React.useMemo(() => {
if (!withAsk) {
return resultsState.results;
}
return withQuestionResult(resultsState.results, query);
}, [resultsState.results, query, withAsk]);
React.useEffect(() => {
if (!query) {
// Reset the cursor when there's no query
setCursor(null);
} else if (results.length > 0) {
// Auto-focus the first result
setCursor(0);
}
}, [results, query]);
// Scroll to the active result.
React.useEffect(() => {
if (cursor === null || !refs.current[cursor]) {
@@ -214,106 +231,93 @@ export const SearchResults = React.forwardRef(function SearchResults(
[moveBy, select]
);
if (resultsState.fetching) {
return (
<div className={tcls('flex', 'items-center', 'justify-center', 'py-8')}>
<Loading className={tcls('w-6', 'text-primary')} />
</div>
);
}
const loading = (
<div className={tcls('flex', 'items-center', 'justify-center', 'p-8')}>
<Loading className={tcls('w-6', 'text-primary-subtle')} />
</div>
);
const noResults = (
<div className={tcls('text', 'text-tint', 'p-8', 'text-center')}>
{t(language, 'search_no_results', query)}
<div className={tcls('text', 'text-tint', 'text-center', 'p-8')}>
<div className="animate-fadeIn" style={{ animationDelay: '0.5s' }}>
{t(language, 'search_no_results', query)}
</div>
</div>
);
return (
<div className={tcls('overflow-auto')}>
{children}
{results.length === 0 ? (
query ? (
noResults
) : null
<AnimatePresence initial={false} mode="wait">
{resultsState.fetching ? (
loading
) : query && results.length === 0 ? (
noResults
) : (
<>
<div data-testid="search-results">
{results.map((item, index) => {
switch (item.type) {
case 'page': {
return (
<SearchPageResultItem
ref={(ref) => {
refs.current[index] = ref;
}}
key={item.id}
query={query}
item={item}
active={index === cursor}
/>
);
}
case 'question': {
return (
<SearchQuestionResultItem
ref={(ref) => {
refs.current[index] = ref;
}}
key={item.id}
question={query}
active={index === cursor}
onClick={onSwitchToAsk}
/>
);
}
case 'recommended-question': {
return (
<SearchQuestionResultItem
ref={(ref) => {
refs.current[index] = ref;
}}
key={item.id}
question={item.question}
active={index === cursor}
onClick={onSwitchToAsk}
recommended
/>
);
}
case 'section': {
return (
<SearchSectionResultItem
ref={(ref) => {
refs.current[index] = ref;
}}
key={item.id}
query={query}
item={item}
active={index === cursor}
/>
);
}
default:
assertNever(item);
<motion.div
layout="position"
className="flex flex-col gap-2 p-4"
data-testid="search-results"
>
{results.map((item, index) => {
switch (item.type) {
case 'page': {
return (
<SearchPageResultItem
ref={(ref) => {
refs.current[index] = ref;
}}
key={item.id}
query={query}
item={item}
active={index === cursor}
/>
);
}
})}
</div>
{!results.some((result) => result.type !== 'question') && noResults}
</>
case 'question': {
return (
<SearchQuestionResultItem
ref={(ref) => {
refs.current[index] = ref;
}}
key={item.id}
question={query}
active={index === cursor}
onClick={onSwitchToAsk}
/>
);
}
case 'recommended-question': {
return (
<SearchQuestionResultItem
ref={(ref) => {
refs.current[index] = ref;
}}
key={item.id}
question={item.question}
active={index === cursor}
onClick={onSwitchToAsk}
recommended
/>
);
}
case 'section': {
return (
<SearchSectionResultItem
ref={(ref) => {
refs.current[index] = ref;
}}
key={item.id}
query={query}
item={item}
active={index === cursor}
/>
);
}
default:
assertNever(item);
}
})}
</motion.div>
)}
</div>
</AnimatePresence>
);
});
/**
* Add a "Ask <question>" item at the top of the results list.
*/
function withQuestionResult(results: ResultType[], query: string): ResultType[] {
const without = results.filter((result) => result.type !== 'question');
if (query.length === 0) {
return without;
}
return [{ type: 'question', id: 'question', query }, ...(without ?? [])];
}
@@ -3,7 +3,8 @@ import React from 'react';
import { tcls } from '@/lib/tailwind';
import { Link } from '../primitives';
import { tString, useLanguage } from '@/intl/client';
import { Button, Link } from '../primitives';
import { HighlightQuery } from './HighlightQuery';
import type { ComputedSectionResult } from './server-actions';
@@ -16,13 +17,15 @@ export const SearchSectionResultItem = React.forwardRef(function SearchSectionRe
ref: React.Ref<HTMLAnchorElement>
) {
const { query, item, active } = props;
const language = useLanguage();
return (
<Link
ref={ref}
href={item.href}
className={tcls(
'[&:has(+:not(&))]:mb-6',
// '[&:has(+:not(&))]:mb-6',
'-mt-2',
'flex',
'items-center',
'pl-6',
@@ -33,6 +36,8 @@ export const SearchSectionResultItem = React.forwardRef(function SearchSectionRe
'font-normal',
'py-2',
'group',
'rounded-lg',
'straight-corners:rounded-none',
active && [
'is-active',
'bg-primary',
@@ -72,21 +77,15 @@ export const SearchSectionResultItem = React.forwardRef(function SearchSectionRe
</p>
) : null}
</div>
<div
className={tcls(
'p-2',
'rounded',
'straight-corners:rounded-none',
'circular-corners:rounded-full',
'bg-primary-solid',
'text-contrast-primary-solid',
'hidden',
'sm:block',
active ? ['opacity-11', 'block'] : ['opacity-0']
)}
>
<Icon icon="arrow-turn-down-left" className={tcls('size-4')} />
</div>
{active ? (
<Button
icon="arrow-turn-down-left"
size="small"
label={tString(language, 'view')}
/>
) : (
<Icon icon="chevron-right" className="size-4 text-tint-subtle/6" />
)}
</Link>
);
});
@@ -28,7 +28,7 @@ const questionWords = new Set([
* Return true if an input query looks like a question.
*/
export function isQuestion(query: string): boolean {
if (query.length > 25 || query.includes('?') || query.includes(' ')) {
if ((query.length > 25 && query.includes(' ')) || query.includes('?')) {
return true;
}
@@ -4,15 +4,17 @@ import { resolvePageId } from '@/lib/pages';
import { findSiteSpaceById, getSiteStructureSections } from '@/lib/sites';
import { filterOutNullable } from '@/lib/typescript';
import { getV1BaseContext } from '@/lib/v1';
import type {
RevisionPage,
SearchAIAnswer,
SearchAIRecommendedQuestionStream,
SearchPageResult,
SearchSpaceResult,
SiteSection,
SiteSectionGroup,
Space,
import {
AIMessageRole,
AIModel,
type RevisionPage,
type SearchAIAnswer,
type SearchAIRecommendedQuestionStream,
type SearchPageResult,
type SearchSpaceResult,
type SiteSection,
type SiteSectionGroup,
type Space,
} from '@gitbook/api';
import type { GitBookBaseContext, GitBookSiteContext } from '@v2/lib/context';
import { fetchServerActionSiteContext, getServerActionBaseContext } from '@v2/lib/server-actions';
@@ -24,6 +26,8 @@ import { isV2 } from '@/lib/v2';
import type { IconName } from '@gitbook/icons';
import { throwIfDataError } from '@v2/lib/data';
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
import { z } from 'zod';
import { streamGenerateDocument, streamGenerateObject } from '../Adaptive/server-actions';
import { DocumentView } from '../DocumentView';
export type OrderedComputedResult = ComputedPageResult | ComputedSectionResult;
@@ -410,3 +414,119 @@ async function transformSitePageResult(
return [page, ...pageSections];
}
/**
* Get an AI-generated answer to a search query.
*/
export async function* streamAISearchSummary({
visitedPages,
}: {
visitedPages: { spaceId: string; pageId: string }[];
}) {
const baseContext = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
const siteURLData = await getSiteURLDataFromMiddleware();
const { response, stream } = await streamGenerateDocument(baseContext, {
organizationId: siteURLData.organization,
siteId: siteURLData.site,
model: AIModel.Fast,
input: [
{
role: AIMessageRole.Developer,
content:
'Summarise the most important information the user has learned from the provided context. Be concise and focus on facts. Do not add commentary, adjectives or other empty descriptors.',
attachments: visitedPages.map(({ spaceId, pageId }) => ({
type: 'page' as const,
spaceId,
pageId,
})),
},
].filter(filterOutNullable),
});
for await (const output of stream) {
yield { output };
}
// Wait for the responseId to be available and yield one final time
const responseId = await response;
yield { responseId };
}
/**
* Get an AI-generated answer to a search query.
*/
export async function* streamAISearchAnswer({
question,
previousResponseId,
}: {
question: string;
previousResponseId?: string;
}) {
const baseContext = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
const siteURLData = await getSiteURLDataFromMiddleware();
const mainResponse = await streamGenerateDocument(baseContext, {
organizationId: siteURLData.organization,
siteId: siteURLData.site,
model: AIModel.Fast,
tools: {
search: true,
getPageContent: true,
},
previousResponseId: previousResponseId,
input: [
{
role: AIMessageRole.Developer,
content:
'Answer the following question by using the provided documentation or by searching. Format the answer in Markdown.',
},
{
role: AIMessageRole.User,
content: question,
},
],
});
for await (const answer of mainResponse.stream) {
yield { answer };
}
// Wait for the responseId to be available and yield one final time
const { responseId } = await mainResponse.response;
yield { responseId };
// Generate a list of follow-up questions
const followupQuestionsResponse = await streamGenerateObject(baseContext, {
organizationId: siteURLData.organization,
siteId: siteURLData.site,
model: AIModel.Fast,
tools: {
search: true,
getPageContent: true,
},
previousResponseId: responseId,
schema: z.object({
followupQuestions: z
.array(z.string())
.describe(
'Follow-up questions to the question, based on the provided content only. Keep questions very short and use pronouns to refer to known concepts.'
)
.max(3),
}),
input: [
{
role: AIMessageRole.Developer,
content: `Based on the previously asked question and the answer, generate a list of follow-up questions. Keep questions very short and use pronouns to refer to known concepts. Check first that you can answer the question given the provided context before listing it as a follow-up question. If you can't answer a question, don't include it in the follow up questions.`,
},
],
});
for await (const partial of followupQuestionsResponse.stream) {
yield {
followupQuestions: (partial.followupQuestions ?? []).filter(
filterOutNullable
) as string[],
};
}
}

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