Compare commits

..

1 Commits

Author SHA1 Message Date
Viktor Renkema 88fe9e97c0 Enable cover heights in gbo 2025-10-13 12:43:17 +02:00
319 changed files with 3826 additions and 6912 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Hide sections if only one is present
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Unify section display condition
+5
View File
@@ -0,0 +1,5 @@
---
"@gitbook/expr": patch
---
Add dev script for @gitbook/expr
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix missing geo data in site insights because of incorrect proxying of events.
@@ -3,4 +3,4 @@
'gitbook': patch
---
Improve OpenAPI circular references
Enhance OpenAPI security scopes handling
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Add stable scroll gutter to search container
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix robots.txt preventing favicon from being indexed.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Redo search scope UI
+6
View File
@@ -0,0 +1,6 @@
---
"@gitbook/react-openapi": patch
"gitbook": patch
---
Highlight discriminator properties in oneOf, allOf, anyOf objects
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Make search accessible
+6
View File
@@ -0,0 +1,6 @@
---
"@gitbook/react-openapi": patch
"gitbook": patch
---
Adapt OpenAPI code samples to prefill API key using visitor data
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix markdownPageURL for PageActions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Improve default site icon
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Use sitespace title instead of space title in search
-6
View File
@@ -1,6 +0,0 @@
---
'@gitbook/openapi-parser': patch
'@gitbook/react-openapi': patch
---
Add x-gitbook-prefix and x-gitbook-token-placeholder for OpenAPI security scheme
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Support nested site section groups
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": minor
---
Show a toolbar for authenticated users with access to the current site.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Add support for inline expressions rendering with visitor data on GBO side
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Update to column width sizing
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": minor
---
Move inline expression evaluation from API to GBO
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix rounded outline items
+5
View File
@@ -0,0 +1,5 @@
---
"@gitbook/react-openapi": patch
---
Ensure operation security requirements inherits from spec-level when not defined
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": minor
---
Add Italian translation
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Expose "Best match" when site has translations
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Improve screen reader accessibility for hints
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Add alt text to inline image
+5
View File
@@ -0,0 +1,5 @@
---
"@gitbook/react-openapi": minor
---
Fix OpenAPISecurities and code sample not using operation security requirements
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix crash for card cover defined without objectFit
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Improve wide page width layout
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Add scrollcontainer component
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix links to other spaces and root page in embeddable view.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Refactor section tabs
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix button content truncate
@@ -2,4 +2,4 @@
'@gitbook/react-openapi': patch
---
Fix OpenAPI basic auth placeholder
Fix merge logic for allOf alternatives
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": minor
---
Add a RSS feed to all pages with update blocks.
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': patch
---
Remove hidden site spaces from navigation
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': patch
---
Fix hidden section not found
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix OpenAPI response dropdown & vertical overflow
+5
View File
@@ -0,0 +1,5 @@
---
"@gitbook/react-openapi": patch
---
Fix OpenAPI response showing as JSON instead of YAML when it should
+6
View File
@@ -0,0 +1,6 @@
---
"@gitbook/react-openapi": patch
"gitbook": patch
---
Fix OpenAPI alternatives not showing
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": minor
---
Scope search across sections and variants
+4 -3
View File
@@ -54,9 +54,10 @@ After forking this repository, you'll want to [create a branch](https://docs.git
#### 3. Install dependencies and run the project locally
##### Prerequisites:
- [Node.js](https://nodejs.org/en) (see "engines" in `package.json`)
- [Bun](https://bun.sh/) (see "packageManager" in `package.json`)
- Node.js (Version: >=20.6)
- Use `nvm` for easy Node management
- [Bun](https://bun.sh/) (Version: >=1.2.15)
- We use a text-based lockfile which isn't supported below 1.2.15
##### Setup steps:
@@ -30,7 +30,7 @@ runs:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
wranglerVersion: '4.10.0'
environment: ${{ inputs.environment }}
command: deployments status --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
@@ -49,7 +49,7 @@ runs:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
wranglerVersion: '4.10.0'
environment: ${{ inputs.environment }}
command: versions deploy ${{ steps.extract_current_version.outputs.version_id }}@100% ${{ inputs.serverVersionId }}@0% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
@@ -61,7 +61,7 @@ runs:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
wranglerVersion: '4.10.0'
environment: ${{ inputs.environment }}
command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
@@ -71,7 +71,7 @@ runs:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
wranglerVersion: '4.10.0'
environment: ${{ inputs.environment }}
command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
@@ -70,7 +70,7 @@ runs:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
wranglerVersion: '4.10.0'
environment: ${{ inputs.environment }}
command: deploy --config ./packages/gitbook/openNext/customWorkers/doWrangler.jsonc
@@ -81,7 +81,7 @@ runs:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
wranglerVersion: '4.10.0'
environment: ${{ inputs.environment }}
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
@@ -104,7 +104,7 @@ runs:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
wranglerVersion: '4.10.0'
environment: ${{ inputs.environment }}
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
+2 -3
View File
@@ -4,9 +4,8 @@ on:
push:
branches:
- main
- changeset-release/main
env:
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN_READONLY }}
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
jobs:
format:
runs-on: ubuntu-latest
@@ -42,7 +41,7 @@ jobs:
name: Build (Open Source)
timeout-minutes: 6
env:
BUN_NPM_TOKEN: ''
NPM_TOKEN_READONLY: ''
steps:
- name: Checkout
uses: actions/checkout@v4
+1 -1
View File
@@ -5,7 +5,7 @@ on:
branches:
- main
env:
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN_READONLY }}
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
jobs:
deploy-v2-vercel:
name: Deploy v2 to Vercel (preview)
+1 -1
View File
@@ -4,7 +4,7 @@ on:
branches:
- main
env:
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN_READONLY }}
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
jobs:
deploy-v2-vercel:
name: Deploy v2 to Vercel (production)
+1 -1
View File
@@ -4,7 +4,7 @@ on:
branches:
- main
env:
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN_READONLY }}
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
jobs:
deploy-v2-vercel:
name: Deploy v2 to Vercel (staging)
+7 -16
View File
@@ -6,7 +6,7 @@ on:
- main
env:
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN_READONLY }}
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
concurrency: ${{ github.workflow }}-${{ github.ref }}
@@ -14,38 +14,29 @@ jobs:
publish:
name: Publish
runs-on: ubuntu-latest
permissions:
id-token: write
pull-requests: write
contents: write
steps:
- name: Checkout Repo
uses: actions/checkout@v3
with:
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
registry-url: 'https://registry.npmjs.org'
# Ensure npm 11.5.1 or later is installed
- name: Update npm
run: npm install -g npm@latest
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Create Release Pull Request
- name: Create Release Pull Request or Publish to npm
id: changesets
uses: changesets/action@v1
with:
publish: bun run publish-all-packages
version: bun run changeset-version
publish: npm run release
version: npm run changeset-version
env:
# Using a PAT instead of GITHUB_TOKEN because we need to run workflows when releases are created
# https://github.com/orgs/community/discussions/26875#discussioncomment-3253761
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
-4
View File
@@ -25,7 +25,3 @@ yarn-error.log*
# TypeScript
*.tsbuildinfo
.npmrc
# Bun pack artifacts
packages/*/*.tgz
+1 -1
View File
@@ -1 +1 @@
v22.3
v20.6
+2 -2
View File
@@ -35,7 +35,7 @@ To run a local version of this project, please follow these simple steps.
### Prerequisites
- Node.js (Version: >= 22.3)
- Node.js (Version: >=20.6)
- Use nvm for easy Node management
- [Bun](https://bun.sh/) (Version: >=1.2.15)
- We use a text-based lockfile which isn't supported below 1.2.15
@@ -90,7 +90,7 @@ If you are GitBook staff, you'll need our NPM token in your local environment.
```
.env.local
BUN_NPM_TOKEN=xxx
NPM_TOKEN_READONLY=xxx
```
and then reinstall dependencies.
+916 -601
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,2 +1,2 @@
[install.scopes]
"gitbook" = { token = "$BUN_NPM_TOKEN", url = "https://registry.npmjs.org" }
"gitbook" = { token = "$NPM_TOKEN_READONLY", url = "https://registry.npmjs.org" }
+10 -28
View File
@@ -1,22 +1,17 @@
{
"name": "gitbook",
"version": "0.1.0",
"engines": {
"node": "^22.3.0"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@changesets/cli": "^2.29.7",
"turbo": "^2.6.1",
"vercel": "^39.4.2"
"@changesets/cli": "^2.27.12",
"turbo": "^2.5.0",
"vercel": "^39.3.0"
},
"packageManager": "bun@1.3.2",
"packageManager": "bun@1.2.15",
"overrides": {
"@codemirror/state": "6.4.1",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"esbuild": "0.24.2"
},
"private": true,
@@ -31,29 +26,16 @@
"e2e": "turbo run e2e",
"e2e-customers": "turbo run e2e-customers",
"changeset": "changeset",
"changeset-version": "changeset version && bun run format && bun update",
"publish-all-packages": "turbo run publish-to-npm --continue=dependencies-successful",
"changeset-version": "changeset version && bun run format",
"release": "turbo run release && changeset publish",
"download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local",
"clean": "turbo run clean"
},
"workspaces": {
"packages": ["packages/*"],
"catalog": {
"@tsconfig/strictest": "^2.0.6",
"@tsconfig/node20": "^20.1.6",
"@gitbook/api": "0.151.0",
"@scalar/api-client-react": "^1.3.46",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"assert-never": "^1.4.0",
"bidc": "^0.0.2",
"bun-types": "^1.1.20",
"classnames": "^2.5.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tsdown": "^0.15.6",
"typescript": "^5.5.3",
"usehooks-ts": "^3.1.1"
"@gitbook/api": "0.143.2",
"bidc": "^0.0.2"
}
},
"patchedDependencies": {
-24
View File
@@ -1,29 +1,5 @@
# @gitbook/browser-types
## 0.1.3
### Patch Changes
- 10995e0: Use NPM Trusted publishing for publishing the package.
- Updated dependencies [10995e0]
- @gitbook/icons@0.3.4
## 0.1.2
### Patch Changes
- 6142d6b: Mark as sideEffects, fix all package bundles
- Updated dependencies [6142d6b]
- @gitbook/icons@0.3.3
## 0.1.1
### Patch Changes
- 295f03d: Republish packages
- Updated dependencies [295f03d]
- @gitbook/icons@0.3.2
## 0.1.0
### Minor Changes
+5 -17
View File
@@ -8,29 +8,17 @@
"default": "./dist/index.js"
}
},
"sideEffects": false,
"version": "0.1.3",
"version": "0.1.0",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/icons": "workspace:"
},
"devDependencies": {
"bun-types": "catalog:",
"tsdown": "catalog:",
"typescript": "catalog:"
"typescript": "^5.5.3"
},
"scripts": {
"build": "tsdown",
"typecheck": "tsc --noEmit",
"dev": "bun run build -- --watch ./src",
"publish-to-npm": "../../scripts/publish-if-new.sh"
"build": "tsc",
"typecheck": "tsc --noEmit"
},
"files": ["dist", "README.md", "CHANGELOG.md"],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"url": "https://github.com/GitbookIO/gitbook"
}
"files": ["dist", "README.md", "CHANGELOG.md"]
}
-18
View File
@@ -1,23 +1,5 @@
# @gitbook/cache-tags
## 0.3.4
### Patch Changes
- 10995e0: Use NPM Trusted publishing for publishing the package.
## 0.3.3
### Patch Changes
- 6142d6b: Mark as sideEffects, fix all package bundles
## 0.3.2
### Patch Changes
- 295f03d: Republish packages
## 0.3.1
### Patch Changes
+7 -17
View File
@@ -4,32 +4,22 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
"development": "./src/index.ts",
"default": "./dist/index.js"
}
},
"sideEffects": false,
"version": "0.3.4",
"version": "0.3.1",
"dependencies": {
"@gitbook/api": "catalog:",
"assert-never": "catalog:"
"assert-never": "^1.2.1"
},
"devDependencies": {
"bun-types": "catalog:",
"tsdown": "catalog:",
"typescript": "catalog:"
"typescript": "^5.5.3"
},
"scripts": {
"build": "tsdown",
"build": "tsc",
"typecheck": "tsc --noEmit",
"dev": "bun run build -- --watch ./src",
"publish-to-npm": "../../scripts/publish-if-new.sh"
"dev": "tsc -w"
},
"files": ["dist", "README.md", "CHANGELOG.md"],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"url": "https://github.com/GitbookIO/gitbook"
}
"files": ["dist", "src", "README.md", "CHANGELOG.md"]
}
-18
View File
@@ -1,23 +1,5 @@
# @gitbook/colors
## 0.4.3
### Patch Changes
- 10995e0: Use NPM Trusted publishing for publishing the package.
## 0.4.2
### Patch Changes
- 6142d6b: Mark as sideEffects, fix all package bundles
## 0.4.1
### Patch Changes
- 295f03d: Republish packages
## 0.4.0
### Minor Changes
+6 -16
View File
@@ -4,28 +4,18 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
"development": "./src/index.ts",
"default": "./dist/index.js"
}
},
"sideEffects": false,
"version": "0.4.3",
"version": "0.4.0",
"devDependencies": {
"bun-types": "catalog:",
"tsdown": "catalog:",
"typescript": "catalog:"
"typescript": "^5.5.3"
},
"scripts": {
"build": "tsdown",
"build": "tsc",
"typecheck": "tsc --noEmit",
"dev": "bun run build -- --watch ./src",
"publish-to-npm": "../../scripts/publish-if-new.sh"
"dev": "tsc -w"
},
"files": ["dist", "README.md", "CHANGELOG.md"],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"url": "https://github.com/GitbookIO/gitbook"
}
"files": ["dist", "src", "README.md", "CHANGELOG.md"]
}
-30
View File
@@ -1,35 +1,5 @@
# @gitbook/embed
## 0.1.5
### Patch Changes
- 10995e0: Use NPM Trusted publishing for publishing the package.
- Updated dependencies [10995e0]
- @gitbook/icons@0.3.4
## 0.1.4
### Patch Changes
- 6142d6b: Mark as sideEffects, fix all package bundles
- Updated dependencies [6142d6b]
- @gitbook/icons@0.3.3
## 0.1.3
### Patch Changes
- 295f03d: Republish packages
- Updated dependencies [295f03d]
- @gitbook/icons@0.3.2
## 0.1.2
### Patch Changes
- 7508674: Fix bundling of packages
## 0.1.1
### Patch Changes
+10 -27
View File
@@ -5,44 +5,27 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./react": {
"types": "./dist/react/index.d.ts",
"default": "./dist/react/index.js"
"default": "./dist/index.js",
"standalone": "./dist/standalone/index.js",
"react": "./dist/react/index.js"
}
},
"sideEffects": false,
"version": "0.1.5",
"version": "0.1.1",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/icons": "workspace:",
"bidc": "catalog:"
},
"peerDependencies": {
"react": "*"
"react": "^18.0.0"
},
"devDependencies": {
"@types/react": "catalog:",
"tsdown": "catalog:",
"typescript": "catalog:",
"react": "catalog:"
"typescript": "^5.5.3",
"react": "^19.0.0"
},
"scripts": {
"build": "bun run build-lib && bun run build-standalone",
"build-lib": "tsdown",
"build-standalone": "bun build src/standalone/index.ts --bundle --minify --outdir=standalone",
"clean": "rm -rf ./dist",
"typecheck": "tsc --noEmit",
"dev": "bun run build -- --watch ./src",
"publish-to-npm": "../../scripts/publish-if-new.sh"
"build": "tsc && bun build src/standalone/index.ts --bundle --minify --outdir=standalone",
"typecheck": "tsc --noEmit"
},
"files": ["dist", "README.md", "CHANGELOG.md"],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"url": "https://github.com/GitbookIO/gitbook"
}
"files": ["dist", "README.md", "CHANGELOG.md", "standalone"]
}
+6 -8
View File
@@ -1,6 +1,4 @@
'use client';
import { useEffect, useMemo, useRef, useState } from 'react';
import React from 'react';
import type {
GetFrameURLOptions,
GitBookEmbeddableConfiguration,
@@ -19,19 +17,19 @@ export type GitBookFrameProps = {
export function GitBookFrame(props: GitBookFrameProps) {
const { className, visitor, buttons, welcomeMessage, suggestions, tools } = props;
const frameRef = useRef<HTMLIFrameElement>(null);
const frameRef = React.useRef<HTMLIFrameElement>(null);
const gitbook = useGitBook();
const [gitbookFrame, setGitbookFrame] = useState<GitBookFrameClient | null>(null);
const [gitbookFrame, setGitbookFrame] = React.useState<GitBookFrameClient | null>(null);
const frameURL = useMemo(() => gitbook.getFrameURL({ visitor }), [gitbook, visitor]);
const frameURL = React.useMemo(() => gitbook.getFrameURL({ visitor }), [gitbook, visitor]);
useEffect(() => {
React.useEffect(() => {
if (frameRef.current) {
setGitbookFrame(gitbook.createFrame(frameRef.current));
}
}, [gitbook]);
useEffect(() => {
React.useEffect(() => {
gitbookFrame?.configure({
buttons,
welcomeMessage,
+1 -12
View File
@@ -29,16 +29,6 @@
--gitbook-widget-easing-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
}
@media (prefers-color-scheme: dark) {
:root {
--gitbook-widget-text-color: #FFFFFF;
--gitbook-widget-border-color: #202020;
--gitbook-widget-background-translucent: rgba(15, 15, 15, 0.9);
--gitbook-widget-background-translucent-hover: rgba(20, 20, 20, 0.9);
--gitbook-widget-background-solid: #f0f0f0;
}
}
* {
box-sizing: border-box;
}
@@ -130,8 +120,7 @@
z-index: 9998;
width: calc(min(var(--gitbook-widget-window-width), calc(100vw - var(--gitbook-widget-right) - var(--gitbook-widget-left))));
height: calc(min(var(--gitbook-widget-window-height), calc(100vh - var(--gitbook-widget-window-bottom) - var(--gitbook-widget-top))));
background-color: var(--gitbook-widget-background-translucent);
backdrop-filter: blur(48px);
background-color: var(--gitbook-widget-background-solid);
border: 1px solid var(--gitbook-widget-border-color);
border-radius: var(--gitbook-widget-radius);
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
-13
View File
@@ -1,13 +0,0 @@
import { defineConfig } from 'tsdown';
export default defineConfig([
{
entry: 'src/index.ts',
outDir: 'dist',
},
{
entry: 'src/react/index.ts',
outDir: 'dist/react',
unbundle: true,
},
]);
+2 -14
View File
@@ -1,25 +1,13 @@
# @gitbook/emoji-codepoints
## 0.2.2
### Patch Changes
- 6142d6b: Mark as sideEffects, fix all package bundles
## 0.2.1
### Patch Changes
- 295f03d: Republish packages
## 0.2.0
### Minor Changes
- 57adb3e: Second release to fix publishing with changeset
- 57adb3e: Second release to fix publishing with changeset
## 0.1.0
### Minor Changes
- 5f8a8fe: Initial release
- 5f8a8fe: Initial release
+3 -3
View File
@@ -1,12 +1,12 @@
{
"name": "@gitbook/emoji-codepoints",
"description": "Optimized mapping of codepoints to the fully qualified emoji codepoints",
"version": "0.2.2",
"version": "0.2.0",
"private": true,
"exports": "./dist/index.ts",
"sideEffects": false,
"dependencies": {},
"devDependencies": {
"emoji-assets": "^9.0.0"
"emoji-assets": "^8.0.0"
},
"scripts": {
"generate": "bun ./build.ts",
-25
View File
@@ -1,30 +1,5 @@
# @gitbook/expr
## 1.2.4
### Patch Changes
- 10995e0: Use NPM Trusted publishing for publishing the package.
- 10995e0: Use NPM dependency for eval-estree-expression
## 1.2.3
### Patch Changes
- 6142d6b: Mark as sideEffects, fix all package bundles
## 1.2.2
### Patch Changes
- 295f03d: Republish packages
## 1.2.1
### Patch Changes
- a629900: Add dev script for @gitbook/expr
## 1.2.0
### Minor Changes
+11 -20
View File
@@ -1,47 +1,38 @@
{
"name": "@gitbook/expr",
"description": "Safely evaluate & parse user-defined GitBook expressions.",
"version": "1.2.4",
"version": "1.2.0",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"sideEffects": false,
"dependencies": {
"eval-estree-expression": "github:jonschlinkert/eval-estree-expression#9cf28d2",
"acorn": "^8.14.0",
"acorn-loose": "8.4.0",
"acorn-walk": "^8.3.4",
"assert-never": "catalog:",
"escodegen": "^2.1.0",
"eval-estree-expression": "github:jonschlinkert/eval-estree-expression#fb0246a"
"assert-never": "^1.2.1"
},
"devDependencies": {
"@tsconfig/strictest": "catalog:",
"@tsconfig/node20": "catalog:",
"@types/escodegen": "^0.0.10",
"bun-types": "^1.1.20",
"tsdown": "^0.15.0",
"@types/estree": "^1.0.6",
"@babel/types": "^7.26.0",
"@types/json-schema": "^7.0.15",
"bun-types": "catalog:",
"tsdown": "catalog:",
"typescript": "catalog:"
"@types/escodegen": "^0.0.10"
},
"scripts": {
"build": "tsdown",
"build": "tsdown --project tsconfig.build.json",
"typecheck": "tsc --noEmit",
"unit": "bun test",
"clean": "rm -rf ./dist",
"dev": "bun run build -- --watch ./src",
"publish-to-npm": "../../scripts/publish-if-new.sh"
"dev": "bun run build -- --watch"
},
"files": ["dist", "README.md", "CHANGELOG.md"],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"url": "https://github.com/GitbookIO/gitbook"
}
"files": ["dist", "README.md", "CHANGELOG.md"]
}
+10
View File
@@ -0,0 +1,10 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": ["./tsconfig.json"],
"exclude": ["**/*.test.ts"],
"compilerOptions": {
"declaration": true,
"noEmit": false,
"outDir": "dist"
}
}
@@ -2,7 +2,8 @@ import { defineConfig } from 'tsdown';
export default defineConfig([
{
entry: ['src/index.ts', 'src/icons.ts'],
unbundle: true,
entry: ['src/index.ts'],
dts: true,
format: ['esm'],
},
]);
-18
View File
@@ -1,23 +1,5 @@
# @gitbook/fonts
## 0.1.3
### Patch Changes
- 10995e0: Use NPM Trusted publishing for publishing the package.
## 0.1.2
### Patch Changes
- 6142d6b: Mark as sideEffects, fix all package bundles
## 0.1.1
### Patch Changes
- 295f03d: Republish packages
## 0.1.0
### Minor Changes
+8 -18
View File
@@ -4,38 +4,28 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
"development": "./src/index.ts",
"default": "./dist/index.js"
}
},
"sideEffects": false,
"version": "0.1.3",
"version": "0.1.0",
"dependencies": {
"@gitbook/api": "catalog:"
},
"devDependencies": {
"bun-types": "catalog:",
"google-font-metadata": "^6.0.7",
"tsdown": "catalog:",
"typescript": "catalog:"
"google-font-metadata": "^6.0.3",
"typescript": "^5.5.3"
},
"scripts": {
"generate": "bun ./bin/generate.js",
"build": "tsdown",
"build": "tsc --project tsconfig.build.json",
"typecheck": "tsc --noEmit",
"dev": "bun run build -- --watch ./src",
"dev": "tsc -w",
"clean": "rm -rf ./dist && rm -rf ./src/data",
"unit": "bun test",
"publish-to-npm": "../../scripts/publish-if-new.sh"
"unit": "bun test"
},
"files": ["dist", "bin", "README.md", "CHANGELOG.md"],
"files": ["dist", "src", "bin", "README.md", "CHANGELOG.md"],
"engines": {
"node": ">=20.0.0"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"url": "https://github.com/GitbookIO/gitbook"
}
}
@@ -1,57 +1,57 @@
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
// 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/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
"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/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff",
"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/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZxhjp-Ek-_0ewmM.woff",
"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/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff",
"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/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
"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/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYAZ9hjp-Ek-_0ew.woff",
"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/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
"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/v49/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAz0klQm_20.woff",
"url": "https://fonts.gstatic.com/s/roboto/v32/KFOmCnqEu92Fr1Mu4mxMKTU1Kg.woff",
}
`;
+4
View File
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "src/**/*.test.ts"]
}
-144
View File
@@ -1,149 +1,5 @@
# gitbook
## 0.20.0
### Minor Changes
- 1d92dc1: Update site layout and blocks with container queries and better transitions
### Patch Changes
- 8fdc5c8: Fix webframe height issue
- 9022d87: Add breadcrumbs to search results
- 0c01832: SearchInput fixes — Translate Clear button, Bold theme color, "Esc" keyboard shortcut
- 1d1f0a7: Fix search container shadow
- 2aa4f2e: Update dropdown menu styling
- a49781e: Printable collapsed code block
- 7d6a316: Revert contentref anchor title
- e27986b: Reword "Everywhere" search scope to "All docs"
- 758b34d: Move the "Ask <query>" to the bottom of search results if query is not a question
- 0ae7cfd: Improve AI Search branding
- f9f8011: Add alt text support to card covers
- ebbb476: Improve ancestor label in references
- 1b0613e: Tweak language dropdown styling
- 3355960: Fix ToggeableLinkItem display
- c31e564: Fix icon resolution for page group
- 55c0b03: Support translations and generic variants together
- 3876f70: Edgeless Assistant window on `depth: flat` sites
- 683bb8f: Add confirmation to "Copy MCP URL" page action
- 4534ea8: Implement unique title tags for sections & variants
- 6d56df7: Treat pages without visible TOC items as `no-toc`
- 8606f22: Add padding to TOC page group items
- 3676d64: Update search scope UI
- c32d449: Fix OpenAPI path display
- 8ce7322: Add OpenAPI servers selection
- 2c3066e: Improve OAuth2 scopes handling in OpenAPI
- 6b1c9fe: Fix opacity of ScrollContainer buttons on bold themes
- Updated dependencies [b4a021a]
- Updated dependencies [a512c90]
- Updated dependencies [df1966d]
- Updated dependencies [b45feaf]
- Updated dependencies [10995e0]
- Updated dependencies [f9f8011]
- Updated dependencies [10995e0]
- Updated dependencies [8ce7322]
- Updated dependencies [2c3066e]
- @gitbook/react-openapi@1.5.3
- @gitbook/openapi-parser@3.0.5
- @gitbook/react-contentkit@0.7.8
- @gitbook/browser-types@0.1.3
- @gitbook/cache-tags@0.3.4
- @gitbook/react-math@0.6.4
- @gitbook/colors@0.4.3
- @gitbook/embed@0.1.5
- @gitbook/fonts@0.1.3
- @gitbook/icons@0.3.4
- @gitbook/expr@1.2.4
## 0.19.2
### Patch Changes
- 6142d6b: Mark as sideEffects, fix all package bundles
- 75543b1: Update minimum Node version in .npmrc
- 511f1cf: Add scroll margin to Expandable
- Updated dependencies [6142d6b]
- @gitbook/emoji-codepoints@0.2.2
- @gitbook/react-contentkit@0.7.7
- @gitbook/openapi-parser@3.0.4
- @gitbook/browser-types@0.1.2
- @gitbook/react-openapi@1.5.2
- @gitbook/cache-tags@0.3.3
- @gitbook/react-math@0.6.3
- @gitbook/colors@0.4.2
- @gitbook/fonts@0.1.2
- @gitbook/icons@0.3.3
- @gitbook/expr@1.2.3
## 0.19.1
### Patch Changes
- 295f03d: Republish packages
- Updated dependencies [bdde392]
- Updated dependencies [295f03d]
- @gitbook/react-contentkit@0.7.6
- @gitbook/browser-types@0.1.1
- @gitbook/cache-tags@0.3.2
- @gitbook/colors@0.4.1
- @gitbook/emoji-codepoints@0.2.1
- @gitbook/expr@1.2.2
- @gitbook/fonts@0.1.1
- @gitbook/icons@0.3.2
- @gitbook/openapi-parser@3.0.3
- @gitbook/react-math@0.6.2
- @gitbook/react-openapi@1.5.1
## 0.19.0
### Minor Changes
- ed7d47d: Show a toolbar for authenticated users with access to the current site.
- 50c1be3: Move inline expression evaluation from API to GBO
- 56e46ce: Add Italian translation
- b932e4e: Scope search across sections and variants
### Patch Changes
- 162cfe4: Hide sections if only one is present
- 1456251: Unify section display condition
- 9344431: Fix missing geo data in site insights because of incorrect proxying of events.
- eea8f1e: Enhance OpenAPI security scopes handling
- 2b8a2d2: Add stable scroll gutter to search container
- b498521: Fix robots.txt preventing favicon from being indexed.
- ceb32b1: Redo search scope UI
- 8e99871: Highlight discriminator properties in oneOf, allOf, anyOf objects
- 2fc2127: Make search accessible
- f3e4041: Adapt OpenAPI code samples to prefill API key using visitor data
- 6815dd1: Fix markdownPageURL for PageActions
- c06b3dd: Improve default site icon
- cba583a: Use sitespace title instead of space title in search
- e434442: Support nested site section groups
- 469b332: Add support for inline expressions rendering with visitor data on GBO side
- 0ef5dc8: Update to column width sizing
- 1e4e54e: Fix rounded outline items
- 379d486: Expose "Best match" when site has translations
- c21693f: Improve screen reader accessibility for hints
- badf3a9: Add alt text to inline image
- c23d4ef: Fix crash for card cover defined without objectFit
- d74f8a7: Improve wide page width layout
- b8388e5: Add scrollcontainer component
- b85eccd: Refactor section tabs
- 1e53f46: Fix button content truncate
- 7a504b7: Fix OpenAPI response dropdown & vertical overflow
- a4c3399: Fix OpenAPI alternatives not showing
- Updated dependencies [a629900]
- Updated dependencies [eea8f1e]
- Updated dependencies [8e99871]
- Updated dependencies [f3e4041]
- Updated dependencies [319a1e5]
- Updated dependencies [529f940]
- Updated dependencies [754cc11]
- Updated dependencies [d7948e3]
- Updated dependencies [a4c3399]
- @gitbook/expr@1.2.1
- @gitbook/react-openapi@1.5.0
## 0.18.0
### Minor Changes
+10 -5
View File
@@ -112,11 +112,11 @@ const testCases: TestsCase[] = [
contentBaseURL: 'https://adiblar.gitbook.io',
tests: [{ name: 'Home', url: '/' }],
},
// {
// name: 'docs.gradient.network',
// contentBaseURL: 'https://docs.gradient.network',
// tests: [{ name: 'Home', url: '/' }],
// },
{
name: 'docs.gradient.network',
contentBaseURL: 'https://docs.gradient.network',
tests: [{ name: 'Home', url: '/' }],
},
// {
// name: 'mygate-network.gitbook.io',
// contentBaseURL: 'https://mygate-network.gitbook.io',
@@ -172,6 +172,11 @@ const testCases: TestsCase[] = [
contentBaseURL: 'https://docs.realapp.link',
tests: [{ name: 'Home', url: '/' }],
},
{
name: 'docs.plaza.finance',
contentBaseURL: 'https://docs.plaza.finance',
tests: [{ name: 'Home', url: '/' }],
},
{
name: 'docs.publicai.io',
contentBaseURL: 'https://docs.publicai.io',
+34 -164
View File
@@ -33,35 +33,9 @@ import {
headerLinks,
runTestCases,
waitForCookiesDialog,
waitForCoverImages,
waitForNotFound,
} from './util';
const AI_PROMPT = `You're being invoked by the GitBook CI/CD pipeline. To make screenshot testing of the GitBook Assistant visually consistent, look up the title of the first page you find and respond with only EXACTLY its title. To find the page title, invoke the search tool with the query "GitBook". Before invoking the search tool, respond with the exact text: "I'm going to look up 'GitBook' and then respond with only the page title.". Do not execute any other tools or output any other text.`;
const overrideAIInitialState = () => {
const greeting = document.querySelector('[data-testid="ai-chat-time-greeting"]');
if (greeting) {
greeting.textContent = 'Good morning';
}
};
const overrideAIResponse = () => {
const userMessage = document.querySelector('[data-testid="ai-chat-message-user"]');
if (userMessage) {
userMessage.textContent = '[Replaced message] Chat message sent by the user';
}
const assistantMessage = document.querySelectorAll(
'[data-testid="ai-chat-message-assistant"] .ai-response-document'
);
assistantMessage.forEach((message) => {
message.innerHTML = '[Replaced message] AI chat response';
});
const suggestions = document.querySelectorAll('[data-testid="ai-chat-followup-suggestion"]');
suggestions.forEach((suggestion) => {
suggestion.textContent = 'Follow-up suggestion';
});
};
const searchTestCases: Test[] = [
{
name: 'Search - AI Mode: None - Complete flow',
@@ -78,9 +52,7 @@ const searchTestCases: Test[] = [
// Fill search input, expecting search results
await searchInput.fill('gitbook');
await expect(page.getByTestId('search-results')).toBeVisible({
timeout: 10_000,
});
await expect(page.getByTestId('search-results')).toBeVisible();
const pageResults = await page.getByTestId('search-page-result').all();
await expect(pageResults.length).toBeGreaterThanOrEqual(1);
const pageSectionResults = await page.getByTestId('search-page-section-result').all();
@@ -125,6 +97,9 @@ const searchTestCases: Test[] = [
await expect(page.getByTestId('search-results')).toBeVisible();
},
},
// TODO: Re-enable the following tests when we have fixed the AI Search timing out:
// - Search - AI Mode: Search - Complete flow
// - Search - AI Mode: Search - URL query (Initial)
{
name: 'Search - AI Mode: Search - URL query (Results)',
url: `${getCustomizationURL({
@@ -132,12 +107,16 @@ const searchTestCases: Test[] = [
mode: CustomizationAIMode.Search,
},
})}&q=gitbook`,
screenshot: false,
run: async (page) => {
await expect(page.getByTestId('search-input')).toBeFocused();
await expect(page.getByTestId('search-input')).toHaveValue('gitbook');
await expect(page.getByTestId('search-results')).toBeVisible();
},
},
// TODO: Re-enable the following tests when we have fixed the AI Search timing out:
// - Ask - AI Mode: Search - URL query (Ask initial)
// - Ask - AI Mode: Search - URL query (Ask results)
{
name: 'Ask - AI Mode: Assistant - Complete flow',
url: getCustomizationURL({
@@ -145,32 +124,25 @@ const searchTestCases: Test[] = [
mode: CustomizationAIMode.Assistant,
},
}),
screenshot: false,
run: async (page) => {
const searchInput = page.locator('css=[data-testid="search-input"]');
// Focus search input, expecting recommended questions
await searchInput.focus();
await expect(page.getByTestId('search-results')).toBeVisible({
timeout: 30_000,
});
const recommendedQuestions = await page
.getByTestId('search-recommended-question')
.all();
await expect(recommendedQuestions.length).toBeGreaterThan(2); // Expect at least 3 questions
// TODO: Re-enable this part of the test when we have fixed the AI Search timing out
// await expect(page.getByTestId('search-results')).toBeVisible();
// const recommendedQuestions = await page
// .getByTestId('search-recommended-question')
// .all();
// await expect(recommendedQuestions.length).toBeGreaterThan(2); // Expect at least 3 questions
// Fill search input, expecting AI search option
await searchInput.fill(AI_PROMPT);
await searchInput.fill('What is gitbook?');
const aiSearchResult = page.getByTestId('search-ask-question');
await expect(aiSearchResult).toBeVisible();
await aiSearchResult.click();
await expect(page.getByTestId('ai-chat')).toBeVisible();
await expect(page.getByTestId('ai-chat-message-user').first()).toHaveText(AI_PROMPT);
await expect(page.getByTestId('ai-chat-message-assistant').first()).toBeVisible();
await expect(page.getByTestId('ai-chat-followup-suggestion')).toHaveCount(3, {
timeout: 60_000,
});
// Override text content for visual consistency in screenshots
await page.evaluate(overrideAIResponse);
},
},
{
@@ -180,12 +152,11 @@ const searchTestCases: Test[] = [
mode: CustomizationAIMode.Assistant,
},
}),
screenshot: false,
run: async (page) => {
await page.keyboard.press('ControlOrMeta+I');
await expect(page.getByTestId('ai-chat')).toBeVisible();
await expect(page.getByTestId('ai-chat-input')).toBeFocused();
// Override text content for visual consistency in screenshots
await page.evaluate(overrideAIInitialState);
},
},
{
@@ -200,8 +171,6 @@ const searchTestCases: Test[] = [
await page.getByTestId('ai-chat-button').click();
await expect(page.getByTestId('ai-chat')).toBeVisible();
await expect(page.getByTestId('ai-chat-input')).toBeFocused();
// Override text content for visual consistency in screenshots
await page.evaluate(overrideAIInitialState);
},
},
{
@@ -211,13 +180,12 @@ const searchTestCases: Test[] = [
mode: CustomizationAIMode.Assistant,
},
})}&ask=`,
screenshot: false,
run: async (page) => {
await expect(page.getByTestId('search-input')).not.toBeFocused();
await expect(page.getByTestId('search-input')).toBeEmpty();
await expect(page.getByTestId('search-input')).not.toHaveValue('What is GitBook?');
await expect(page.getByTestId('ai-chat')).toBeVisible();
await expect(page.getByTestId('ai-chat-input')).toBeFocused();
// Override text content for visual consistency in screenshots
await page.evaluate(overrideAIInitialState);
},
},
{
@@ -226,18 +194,17 @@ const searchTestCases: Test[] = [
ai: {
mode: CustomizationAIMode.Assistant,
},
})}&ask=${encodeURIComponent(AI_PROMPT)}`,
})}&ask=What+is+GitBook%3F`,
screenshot: false,
run: async (page) => {
await expect(page.getByTestId('search-input')).not.toBeFocused();
await expect(page.getByTestId('search-input')).not.toHaveValue('What is GitBook?');
await expect(page.getByTestId('ai-chat')).toBeVisible();
await expect(page.getByTestId('ai-chat-message-user').first()).toHaveText(AI_PROMPT);
await expect(page.getByTestId('ai-chat-message-assistant').first()).toBeVisible();
await expect(page.getByTestId('ai-chat-followup-suggestion')).toHaveCount(3, {
timeout: 60_000,
await expect(page.getByTestId('ai-chat')).toBeVisible({
timeout: 15_000,
});
// Override text content for visual consistency in screenshots
await page.evaluate(overrideAIResponse);
await expect(page.getByTestId('ai-chat-message').first()).toHaveText(
'What is GitBook?'
);
},
},
];
@@ -261,58 +228,6 @@ const testCases: TestsCase[] = [
);
},
},
{
name: 'Expandable TOC navigation',
url: '',
run: async (page) => {
await waitForCookiesDialog(page);
// Verify "Navigation" link is not visible initially
const navigationLink = page.getByRole('link', { name: 'Navigation' });
await expect(navigationLink).not.toBeVisible();
// Find and click the chevron element that is next to "Editor" in the TOC
// It is a span inside the link
const editorChevron = page
.getByRole('link', { name: 'Editor' })
.locator('span');
await editorChevron.click();
// Verify "Navigation" link becomes visible after expansion
await expect(navigationLink).toBeVisible();
},
},
{
name: 'Expandable nested TOC navigation',
url: '',
screenshot: false,
run: async (page) => {
await waitForCookiesDialog(page);
// Verify "Spaces" link is not visible initially
const navigationLink = page.getByRole('link', { name: 'Spaces' });
await expect(navigationLink).not.toBeVisible();
// Find and click the chevron element that is next to "Editor" in the TOC
// It is a span inside the link
const editorChevron = page
.getByRole('link', { name: 'Editor' })
.locator('span');
await editorChevron.click();
// At this stage the link should still not be visible
await expect(navigationLink).not.toBeVisible();
// Then we click 'Content Structure' chevron to expand further
const contentStructureChevron = page
.getByRole('link', { name: 'Content Structure' })
.locator('span');
await contentStructureChevron.click();
// Verify "Spaces" link becomes visible after expansion
await expect(navigationLink).toBeVisible();
},
},
...searchTestCases,
{
name: 'Not found',
@@ -370,37 +285,6 @@ const testCases: TestsCase[] = [
).toBeVisible();
},
},
{
name: 'Switch variant with alternate link in metadata',
url: 'rfcs',
run: async (page) => {
const spaceDropdown = page
.locator('[data-testid="space-dropdown-button"]')
.locator('visible=true');
await spaceDropdown.click();
const variantSelectionDropdown = page.locator(
'css=[data-testid="dropdown-menu"]'
);
// Click the variant space called 'Multi-Variants' for which
// there is an alternate link in the current (RFC variant) page metadata
await variantSelectionDropdown
.getByRole('menuitem', {
name: 'Multi-Variants',
})
.click();
// It should navigate to the alternate link defined in the metadata (a completely different page)
await page.waitForURL((url) =>
url.pathname.includes('multi-variants/reference/api-reference/pets')
);
// Verify we are on the correct page by checking the h1
await expect(
page.getByRole('heading', { level: 1, name: 'Pets' })
).toBeVisible();
},
},
],
},
{
@@ -843,10 +727,7 @@ const testCases: TestsCase[] = [
url: 'blocks/integrations',
run: async (page) => {
await waitForCookiesDialog(page);
const mermaidIframe = page
.locator('iframe[title*="mermaid"]')
.first()
.contentFrame();
const mermaidIframe = page.locator('iframe[title*="mermaid"]').contentFrame();
await expect(mermaidIframe.getByText('Mermaid', { exact: true })).toBeVisible();
await expect(mermaidIframe.getByText('Diagram', { exact: true })).toBeVisible();
},
@@ -912,11 +793,6 @@ const testCases: TestsCase[] = [
url: 'blocks/cards',
fullPage: true,
},
{
name: 'Updates',
url: 'blocks/updates',
fullPage: true,
},
{
name: 'Math',
url: 'blocks/math',
@@ -975,10 +851,7 @@ const testCases: TestsCase[] = [
{
name: 'With cover',
url: 'page-options/page-with-cover',
run: async (page) => {
await waitForCookiesDialog(page);
await waitForCoverImages(page);
},
run: waitForCookiesDialog,
},
{
name: 'With cover for dark mode',
@@ -993,18 +866,12 @@ const testCases: TestsCase[] = [
{
name: 'With hero cover',
url: 'page-options/page-with-hero-cover',
run: async (page) => {
await waitForCookiesDialog(page);
await waitForCoverImages(page);
},
run: waitForCookiesDialog,
},
{
name: 'With cover and no TOC',
url: 'page-options/page-with-cover-and-no-toc',
run: async (page) => {
await waitForCookiesDialog(page);
await waitForCoverImages(page);
},
run: waitForCookiesDialog,
screenshot: {
waitForTOCScrolling: false,
},
@@ -1587,7 +1454,10 @@ const testCases: TestsCase[] = [
locale,
},
}),
run: waitForCookiesDialog,
run: async (page) => {
const dialog = page.getByTestId('cookies-dialog');
await expect(dialog).toBeVisible();
},
})),
},
{
+1 -11
View File
@@ -143,10 +143,7 @@ export const headerLinks: CustomizationHeaderItem[] = [
export async function waitForCookiesDialog(page: Page) {
const dialog = page.getByTestId('cookies-dialog');
await expect(dialog).toBeVisible({
// Cookies dialog may take some times to appear
timeout: 10_000,
});
await expect(dialog).toBeVisible();
}
export async function waitForNotFound(_page: Page, response: Response | null) {
@@ -154,13 +151,6 @@ export async function waitForNotFound(_page: Page, response: Response | null) {
expect(response?.status()).toBe(404);
}
export async function waitForCoverImages(page: Page) {
// Wait for cover images to exist (not the shimmer placeholder)
await expect(page.locator('img[alt="Page cover"]').first()).toBeVisible({
timeout: 10_000,
});
}
/**
* Transform test cases into Playwright tests and run it.
*/
+2 -4
View File
@@ -7,8 +7,7 @@ export default {
converter: 'edge',
proxyExternalRequest: 'fetch',
queue: () => import('./openNext/queue/middleware').then((m) => m.default),
incrementalCache: () =>
import('./openNext/incrementalCache/server').then((m) => m.default),
incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default),
tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
},
},
@@ -19,8 +18,7 @@ export default {
converter: 'edge',
proxyExternalRequest: 'fetch',
queue: () => import('./openNext/queue/middleware').then((m) => m.default),
incrementalCache: () =>
import('./openNext/incrementalCache/middleware').then((m) => m.default),
incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default),
tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
},
},
@@ -1,13 +1,15 @@
{
"main": "default.js",
"name": "gitbook-open-v2-server",
"keep_names": false,
"compatibility_date": "2025-04-14",
"compatibility_flags": [
"nodejs_compat",
"allow_importable_env",
"global_fetch_strictly_public"
],
"observability": {
"enabled": true
},
"vars": {
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
},
@@ -2,12 +2,14 @@
"main": "do.js",
"name": "gitbook-open-v2-do",
"compatibility_date": "2025-04-14",
"keep_names": false,
"compatibility_flags": [
"nodejs_compat",
"allow_importable_env",
"global_fetch_strictly_public"
],
"observability": {
"enabled": true
},
"env": {
"preview": {
"vars": {
@@ -2,7 +2,6 @@
"main": "middleware.js",
"name": "gitbook-open-v2",
"compatibility_date": "2025-04-14",
"keep_names": false,
"compatibility_flags": [
"nodejs_compat",
"allow_importable_env",
@@ -12,6 +11,9 @@
"directory": "../../.open-next/assets",
"binding": "ASSETS"
},
"observability": {
"enabled": true
},
"vars": {
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
},
@@ -7,6 +7,8 @@ import type {
} from '@opennextjs/aws/types/overrides.js';
import { getCloudflareContext } from '@opennextjs/cloudflare';
import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
import type { DurableObjectNamespace, Rpc } from '@cloudflare/workers-types';
export const BINDING_NAME = 'NEXT_INC_CACHE_R2_BUCKET';
@@ -21,7 +23,7 @@ export type KeyOptions = {
* It is very similar to the `R2IncrementalCache` in the `@opennextjs/cloudflare` package, but it has an additional
* R2WriteBuffer Durable Object to handle writes to R2. Given how we set up cache, we often end up writing to the same key too fast.
*/
export class GitbookIncrementalCache implements IncrementalCache {
class GitbookIncrementalCache implements IncrementalCache {
name = 'GitbookIncrementalCache';
async get<CacheType extends CacheEntryType = 'cache'>(
@@ -135,3 +137,12 @@ export class GitbookIncrementalCache implements IncrementalCache {
);
}
}
export default withRegionalCache(new GitbookIncrementalCache(), {
mode: 'long-lived',
// We can do it because we use our own logic to invalidate the cache
bypassTagCacheOnCacheHit: true,
defaultLongLivedTtlSec: 60 * 60 * 24 /* 24 hours */,
// We don't want to update the cache entry on every cache hit
shouldLazilyUpdateOnCacheHit: false,
});
@@ -1,11 +0,0 @@
import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
import { GitbookIncrementalCache } from './incrementalCache';
export default withRegionalCache(new GitbookIncrementalCache(), {
mode: 'long-lived',
// We can do it because we use our own logic to invalidate the cache
bypassTagCacheOnCacheHit: true,
defaultLongLivedTtlSec: 60 * 60 * 24 /* 24 hours */,
// We don't want to update the cache entry on every cache hit
shouldLazilyUpdateOnCacheHit: false,
});
@@ -1,12 +0,0 @@
import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
import { GitbookIncrementalCache } from './incrementalCache';
export default withRegionalCache(new GitbookIncrementalCache(), {
mode: 'long-lived',
// Because of a race condition, the middleware may have populated the cache entry before `cache.match` had time to run on the server.
// TODO: We should bypass the incremental cache entirely when the interceptor has caught the request. Should be done in OpenNext.
bypassTagCacheOnCacheHit: false,
defaultLongLivedTtlSec: 60 * 60 * 24 /* 24 hours */,
// We don't want to update the cache entry on every cache hit
shouldLazilyUpdateOnCacheHit: false,
});
+17 -27
View File
@@ -1,25 +1,22 @@
{
"name": "gitbook",
"version": "0.20.0",
"version": "0.18.0",
"private": true,
"dependencies": {
"@cloudflare/workers-types": "^4.20251011.0",
"@gitbook/api": "catalog:",
"@gitbook/expr": "workspace:*",
"@gitbook/browser-types": "workspace:*",
"@gitbook/cache-tags": "workspace:*",
"@gitbook/colors": "workspace:*",
"@gitbook/embed": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*",
"@gitbook/expr": "workspace:*",
"@gitbook/fonts": "workspace:*",
"@gitbook/icons": "workspace:*",
"@gitbook/openapi-parser": "workspace:*",
"@gitbook/react-contentkit": "workspace:*",
"@gitbook/react-math": "workspace:*",
"@gitbook/react-openapi": "workspace:*",
"@modelcontextprotocol/sdk": "1.17.5",
"@opennextjs/aws": "^3.8.5",
"@opennextjs/cloudflare": "^1.11.0",
"@modelcontextprotocol/sdk": "^1.17.5",
"@opennextjs/cloudflare": "^1.8.3",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-hover-card": "^1.1.15",
@@ -31,16 +28,19 @@
"@tailwindcss/typography": "^0.5.16",
"@tusbar/cache-control": "^1.0.2",
"ai": "^4.2.2",
"assert-never": "catalog:",
"assert-never": "^1.2.1",
"bidc": "catalog:",
"classnames": "catalog:",
"bun-types": "^1.1.20",
"classnames": "^2.5.1",
"direction": "^2.0.1",
"event-iterator": "^2.0.0",
"feed": "^5.1.0",
"framer-motion": "^10.16.14",
"image-size": "^2.0.2",
"js-cookie": "^3.0.5",
"jsontoxml": "^1.0.1",
"jwt-decode": "^4.0.0",
"katex": "^0.16.9",
"mathjax": "^3.2.2",
"mcp-handler": "^1.0.2",
"mdast-util-from-markdown": "^2.0.2",
"mdast-util-frontmatter": "^2.0.1",
@@ -49,7 +49,7 @@
"memoizee": "^0.4.17",
"micromark-extension-frontmatter": "^2.0.0",
"micromark-extension-gfm": "^3.0.0",
"motion": "^12.23.24",
"motion": "^12.23.12",
"next": "15.4.0",
"next-themes": "^0.4.6",
"nuqs": "^2.2.3",
@@ -58,8 +58,6 @@
"openapi-types": "^12.1.3",
"p-map": "^7.0.3",
"quick-lru": "^7.0.1",
"react": "catalog:",
"react-dom": "catalog:",
"react-hotkeys-hook": "^4.4.1",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.1",
@@ -75,7 +73,7 @@
"unist-util-remove": "^4.0.0",
"unist-util-visit": "^5.0.0",
"url-join": "^5.0.0",
"usehooks-ts": "catalog:",
"usehooks-ts": "^3.1.0",
"warn-once": "^0.1.1",
"zod": "^3",
"zustand": "^5.0.3"
@@ -83,7 +81,6 @@
"devDependencies": {
"@argos-ci/playwright": "^5.0.9",
"@playwright/test": "^1.54.2",
"@scalar/api-client-react": "catalog:",
"@tailwindcss/postcss": "^4.1.11",
"@types/js-cookie": "^3.0.6",
"@types/jsontoxml": "^1.0.5",
@@ -92,10 +89,9 @@
"@types/node": "^20",
"@types/object-hash": "^3.0.6",
"@types/parse-cache-control": "^1.0.4",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@types/react": "18.3.13",
"@types/react-dom": "18.3.1",
"@types/rison": "^0.0.9",
"bun-types": "catalog:",
"deepmerge": "^4.3.1",
"env-cmd": "^10.1.0",
"jsonwebtoken": "^9.0.2",
@@ -103,10 +99,8 @@
"stylelint": "^16.16.0",
"tailwindcss": "^4.1.11",
"ts-essentials": "^10.0.1",
"typescript": "catalog:",
"vercel": "^39.3.0",
"wrangler": "^4.43.0",
"rss-parser": "^3.13.0"
"typescript": "^5.5.3",
"vercel": "^39.3.0"
},
"scripts": {
"generate": "./scripts/generate.sh",
@@ -127,9 +121,5 @@
},
"browserslist": [
">0.3%, chrome >= 64, edge >= 79, firefox >= 67, opera >= 51, safari >= 12 and not dead"
],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
]
}
@@ -22,11 +22,7 @@ export default async function SiteDynamicLayout({
const withTracking = shouldTrackEvents(await headers());
return (
<CustomizationRootLayout
className="site-background"
forcedTheme={forcedTheme}
context={context}
>
<CustomizationRootLayout forcedTheme={forcedTheme} context={context}>
<SiteLayout
context={context}
forcedTheme={forcedTheme}
@@ -1,7 +1,16 @@
import type { RouteLayoutParams } from '@/app/utils';
import { EmbeddableAssistantPage } from '@/components/Embeddable';
import { getEmbeddableDynamicContext } from '@/lib/embeddable';
export const dynamic = 'force-static';
export default async function Page() {
return <EmbeddableAssistantPage />;
type PageProps = {
params: Promise<RouteLayoutParams>;
};
export default async function Page(props: PageProps) {
const params = await props.params;
const { context } = await getEmbeddableDynamicContext(params);
return <EmbeddableAssistantPage context={context} />;
}
@@ -19,7 +19,7 @@ export default async function SiteStaticLayout({
const withTracking = shouldTrackEvents();
return (
<CustomizationRootLayout className="site-background" context={context}>
<CustomizationRootLayout context={context}>
<SiteLayout
context={context}
withTracking={withTracking}
@@ -1,7 +1,16 @@
import type { RouteLayoutParams } from '@/app/utils';
import { EmbeddableAssistantPage } from '@/components/Embeddable';
import { getEmbeddableStaticContext } from '@/lib/embeddable';
export const dynamic = 'force-static';
export default async function Page() {
return <EmbeddableAssistantPage />;
type PageProps = {
params: Promise<RouteLayoutParams>;
};
export default async function Page(props: PageProps) {
const params = await props.params;
const { context } = await getEmbeddableStaticContext(params);
return <EmbeddableAssistantPage context={context} />;
}
@@ -1,12 +0,0 @@
import { type RouteParams, getPagePathFromParams, getStaticSiteContext } from '@/app/utils';
import { servePageRSS } from '@/routes/rss';
import type { NextRequest } from 'next/server';
export const dynamic = 'force-static';
export async function GET(_request: NextRequest, { params }: { params: Promise<RouteParams> }) {
const { context } = await getStaticSiteContext(await params);
const pathname = getPagePathFromParams(await params);
return servePageRSS(context, pathname);
}
@@ -16,14 +16,14 @@ export function AIMessageView(
) {
const { message, context, withToolCalls = true, withLinkPreviews = true } = props;
return message.steps.length > 0 ? (
return (
<div className="flex flex-col gap-2">
{message.steps.map((step, index) => {
return (
<div
key={index}
className={tcls(
'flex flex-col gap-2',
'flex animate-fade-in-slow flex-col gap-2',
step.content.nodes.length > 0 ? 'has-content' : ''
)}
>
@@ -35,7 +35,7 @@ export function AIMessageView(
wrapBlocksInSuspense: false,
withLinkPreviews,
}}
style="ai-response-document mt-2 space-y-4 *:origin-top-left *:animate-blur-in-slow"
style="mt-2 space-y-4 empty:hidden"
/>
{withToolCalls && step.toolCalls && step.toolCalls.length > 0 ? (
@@ -45,5 +45,5 @@ export function AIMessageView(
);
})}
</div>
) : null;
);
}
@@ -39,7 +39,7 @@ function ToolCallSummary(props: { toolCall: AIToolCall; context: GitBookSiteCont
const { toolCall, context } = props;
return (
<div className="mt-2 flex origin-top-left animate-blur-in-slow items-start gap-2 text-sm text-tint-subtle">
<div className="flex origin-left animate-scale-in-slow items-start gap-2 text-sm text-tint-subtle">
<Icon
icon={getIconForToolCall(toolCall)}
className="mt-1 size-3 shrink-0 text-tint-subtle/8"
@@ -160,7 +160,7 @@ async function DescriptionForSearchToolCall(props: {
const hasResults = toolCall.results.length > 0;
return (
<details className="-ml-5 group flex w-full flex-col">
<details className={tcls('-ml-5 group flex w-full flex-col', hasResults ? 'gap-2' : '')}>
<summary
className={tcls(
'-mx-2 flex list-none items-center gap-2 circular-corners:rounded-2xl rounded-corners:rounded-md pr-4 pl-7 transition-colors marker:hidden',
@@ -187,7 +187,7 @@ async function DescriptionForSearchToolCall(props: {
) : null}
</summary>
{hasResults ? (
<div className="hide-scrollbar mt-4 max-h-0 overflow-y-auto circular-corners:rounded-2xl rounded-corners:rounded-lg border border-tint-subtle p-2 opacity-0 transition-all transition-discrete duration-500 group-open:max-h-96 group-open:opacity-11">
<div className="hide-scrollbar mt-1 max-h-0 overflow-y-auto circular-corners:rounded-2xl rounded-corners:rounded-lg border border-tint-subtle p-2 opacity-0 transition-all transition-discrete duration-500 group-open:max-h-96 group-open:opacity-11">
<ol className="space-y-1">
{searchResultsWithHrefs.map((result, index) => (
<li
+12 -3
View File
@@ -1,14 +1,14 @@
'use client';
import { CustomizationAIMode } from '@gitbook/api';
import { Icon, type IconName } from '@gitbook/icons';
import { Icon, type IconName, IconStyle } from '@gitbook/icons';
import * as React from 'react';
import type { ReactNode } from 'react';
import { tString, useLanguage } from '@/intl/client';
import type { GitBookAssistant } from '@gitbook/browser-types';
import { useAIChatController, useAIChatState } from '.';
import { AIChatIcon, AISearchIcon, getAIChatName } from '../AIChat';
import { AIChatIcon, getAIChatName } from '../AIChat';
import { useIntegrationAssistants } from '../Integrations';
import { useSearch } from '../Search/useSearch';
@@ -104,7 +104,16 @@ export function useAI(): AIContext {
assistants.push({
id: 'gitbook-ai-search',
label: tString(language, 'ai_chat_context_badge'),
icon: <AISearchIcon />,
icon: (
<div className="relative">
<Icon icon="search" className="size-4" />
<Icon
icon="sparkle"
iconStyle={IconStyle.Solid}
className="absolute top-[2.5px] left-[2.6px] size-2"
/>
</div>
),
open: (query?: string) => {
if (query) {
setSearchState((prev) =>
@@ -2,14 +2,12 @@
import { t, tString, useLanguage } from '@/intl/client';
import type { TranslationLanguage } from '@/intl/translations';
import { tcls } from '@/lib/tailwind';
import { Icon } from '@gitbook/icons';
import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import {
type AIChatController,
type AIChatState,
useAI,
useAIChatController,
useAIChatState,
} from '../AI';
@@ -25,15 +23,15 @@ import {
import { useTrackEvent } from '../Insights';
import { useNow } from '../hooks';
import { Button } from '../primitives';
import { ScrollContainer } from '../primitives/ScrollContainer';
import { AIChatControlButton } from './AIChatControlButton';
import { AIChatIcon } from './AIChatIcon';
import { AIChatInput } from './AIChatInput';
import { AIChatMessages } from './AIChatMessages';
import AIChatSuggestedQuestions from './AIChatSuggestedQuestions';
export function AIChat() {
const { config } = useAI();
export function AIChat(props: { trademark: boolean }) {
const { trademark } = props;
const language = useLanguage();
const chat = useAIChatState();
const chatController = useAIChatController();
@@ -67,22 +65,21 @@ export function AIChat() {
}
}, [chat.opened, trackEvent]);
if (!chat.opened) {
return null;
}
return (
<div
data-testid="ai-chat"
className={tcls(
'ai-chat inset-y-0 right-0 z-40 mx-auto flex max-w-3xl scroll-mt-36 px-4 py-4 transition-[width,opacity,margin,display] transition-discrete duration-300 sm:px-6 lg:fixed lg:w-80 lg:p-0 xl:w-96',
chat.opened
? 'lg:starting:ml-0 lg:starting:w-0 lg:starting:opacity-0'
: 'hidden lg:ml-0 lg:w-0! lg:opacity-0'
)}
className="ai-chat inset-y-0 right-0 z-40 mx-auto flex max-w-3xl animate-present scroll-mt-36 px-4 py-4 transition-all duration-300 sm:px-6 lg:fixed lg:w-80 lg:animate-enter-from-right lg:pr-4 lg:pl-0 xl:w-96"
>
<EmbeddableFrame className="relative shrink-0 border-tint-subtle border-l to-tint-base transition-all duration-300 max-lg:circular-corners:rounded-3xl max-lg:rounded-corners:rounded-md max-lg:border lg:w-80 xl:w-96">
<EmbeddableFrame className="relative circular-corners:rounded-3xl rounded-corners:rounded-md depth-subtle:shadow-lg shadow-tint ring-1 ring-tint-subtle">
<EmbeddableFrameHeader>
<AIChatDynamicIcon trademark={config.trademark} />
<AIChatDynamicIcon trademark={trademark} />
<EmbeddableFrameHeaderMain>
<EmbeddableFrameTitle>
{getAIChatName(language, config.trademark)}
{getAIChatName(language, trademark)}
</EmbeddableFrameTitle>
<AIChatSubtitle chat={chat} />
</EmbeddableFrameHeaderMain>
@@ -99,7 +96,7 @@ export function AIChat() {
</EmbeddableFrameButtons>
</EmbeddableFrameHeader>
<EmbeddableFrameBody>
<AIChatBody chatController={chatController} chat={chat} />
<AIChatBody chatController={chatController} chat={chat} trademark={trademark} />
</EmbeddableFrameBody>
</EmbeddableFrame>
</div>
@@ -146,33 +143,10 @@ export function AIChatSubtitle(props: {
const language = useLanguage();
return (
<EmbeddableFrameSubtitle
className={tcls('relative', chat.loading ? 'h-3 opacity-11' : 'h-0 opacity-0')}
>
<span
className={tcls(
'absolute left-0',
chat.loading
? chat.messages[chat.messages.length - 1]?.content
? 'animate-blur-in-slow'
: 'hidden'
: 'animate-blur-out-slow'
)}
>
{t(language, 'ai_chat_working')}
</span>
<span
className={tcls(
'absolute left-0',
chat.loading
? chat.messages[chat.messages.length - 1]?.content
? 'animate-blur-out-slow'
: 'animate-blur-in-slow'
: 'hidden'
)}
>
{t(language, 'ai_chat_thinking')}
</span>
<EmbeddableFrameSubtitle className={chat.loading ? 'h-3 opacity-11' : 'h-0 opacity-0'}>
{chat.messages[chat.messages.length - 1]?.content
? tString(language, 'ai_chat_working')
: tString(language, 'ai_chat_thinking')}
</EmbeddableFrameSubtitle>
);
}
@@ -183,13 +157,20 @@ export function AIChatSubtitle(props: {
export function AIChatBody(props: {
chatController: AIChatController;
chat: AIChatState;
trademark: boolean;
welcomeMessage?: string;
suggestions?: string[];
}) {
const { chatController, chat, suggestions } = props;
const { trademark } = useAI().config;
const { chatController, chat, trademark, suggestions } = props;
const [input, setInput] = React.useState('');
const scrollContainerRef = React.useRef<HTMLDivElement>(null);
// Ref for the last user message element
const lastUserMessageRef = React.useRef<HTMLDivElement>(null);
const inputRef = React.useRef<HTMLDivElement>(null);
const [inputHeight, setInputHeight] = React.useState(0);
const language = useLanguage();
const now = useNow(60 * 60 * 1000); // Refresh every hour for greeting
@@ -203,43 +184,67 @@ export function AIChatBody(props: {
return tString(language, 'ai_chat_assistant_greeting_evening');
}, [now, language]);
// Auto-scroll to the latest user message when messages change
React.useEffect(() => {
if (chat.messages.length > 0 && lastUserMessageRef.current) {
lastUserMessageRef.current.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
}, [chat.messages.length]);
React.useEffect(() => {
const timeout = setTimeout(() => {
if (lastUserMessageRef.current) {
lastUserMessageRef.current.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
}, 100);
// We want the chat messages to scroll underneath the input, but they should scroll past the input when scrolling all the way down.
// The best way to do this is to observe the input height and adjust the padding bottom of the scroll container accordingly.
const observer = new ResizeObserver((entries) => {
entries.forEach((entry) => {
setInputHeight(entry.contentRect.height + 32);
});
});
if (inputRef.current) {
observer.observe(inputRef.current);
}
return () => {
observer.disconnect();
clearTimeout(timeout);
};
}, []);
return (
<>
<ScrollContainer
className="shrink grow basis-80 animate-fade-in-slow [container-type:size]"
contentClassName="p-4 gutter-stable flex flex-col gap-4"
orientation="vertical"
fadeEdges={['leading']}
active={`message-group-${chat.messages.filter((message) => message.role === 'user').length - 1}`}
<div
ref={scrollContainerRef}
className="gutter-stable flex grow scroll-pt-4 flex-col gap-4 overflow-y-auto p-4"
style={{
paddingBottom: `${inputHeight}px`,
}}
>
{isEmpty ? (
<div className="flex grow flex-col">
<div className="my-auto flex flex-row items-center gap-4 pb-6 [@container(min-height:400px)]:flex-col">
<div
className="flex size-16 shrink-0 animate-scale-in items-center justify-center rounded-full bg-primary-solid/1 [@container(min-height:400px)]:size-32"
style={{ animationDelay: '.3s' }}
>
<AIChatIcon
state="intro"
trademark={trademark}
className="size-8 text-primary [@container(min-height:400px)]:size-16"
/>
</div>
<div className="flex flex-col items-start [@container(min-height:400px)]:items-center">
<h5
className="animate-blur-in-slow font-bold text-lg text-tint-strong [@container(min-height:400px)]:text-center"
style={{ animationDelay: '.5s' }}
data-testid="ai-chat-time-greeting"
>
{timeGreeting}
</h5>
<p
className="animate-blur-in-slow text-tint [@container(min-height:400px)]:text-center"
style={{ animationDelay: '.6s' }}
>
{t(language, 'ai_chat_assistant_description')}
</p>
</div>
<div className="flex min-h-full w-full shrink-0 flex-col items-center justify-center gap-6 py-4">
<div className="flex size-32 animate-fade-in-slow items-center justify-center rounded-full bg-tint-subtle">
<AIChatIcon
state="intro"
trademark={trademark}
className="size-16 animate-[present_500ms_200ms_both]"
/>
</div>
<div className="animate-[fadeIn_500ms_400ms_both]">
<h5 className=" text-center font-bold text-lg text-tint-strong">
{timeGreeting}
</h5>
<p className="text-center text-tint">
{t(language, 'ai_chat_assistant_description')}
</p>
</div>
{!chat.error ? (
<AIChatSuggestedQuestions
@@ -249,11 +254,17 @@ export function AIChatBody(props: {
) : null}
</div>
) : (
<AIChatMessages chat={chat} chatController={chatController} />
<AIChatMessages
chat={chat}
chatController={chatController}
lastUserMessageRef={lastUserMessageRef}
/>
)}
</ScrollContainer>
<div className="flex flex-col gap-2 px-4 pb-4">
</div>
<div
ref={inputRef}
className="absolute inset-x-0 bottom-0 mr-2 flex select-none flex-col gap-4 bg-linear-to-b from-transparent to-50% to-tint-base/9 p-4 pr-2"
>
{/* Display an error banner when something went wrong. */}
{chat.error ? <AIChatError chatController={chatController} /> : null}
@@ -23,7 +23,7 @@ export function AIChatButton(props: {
iconOnly={!showLabel}
size="medium"
variant="header"
className="h-9 px-2.5 @max-2xl:[&_.button-content]:hidden"
className="h-9 px-2.5 max-md:[&_.button-content]:hidden"
label={
<div className="flex items-center gap-2">
{t(language, 'ai_chat_ask', assistant.label)}
@@ -1,5 +1,5 @@
import { tcls } from '@/lib/tailwind';
import { Icon, IconStyle } from '@gitbook/icons';
import { Icon } from '@gitbook/icons';
import type React from 'react';
interface AIChatIconProps extends React.SVGProps<SVGSVGElement> {
@@ -9,18 +9,17 @@ interface AIChatIconProps extends React.SVGProps<SVGSVGElement> {
trademark?: boolean;
}
export function AIChatIcon({
export const AIChatIcon = ({
className = 'size-4',
size,
trademark = true,
state = 'default',
...props
}: AIChatIconProps) {
}: AIChatIconProps) => {
if (!trademark) {
return (
<Icon
icon="sparkle"
{...props}
className={tcls(
className,
(state === 'thinking' || state === 'working') &&
@@ -164,36 +163,4 @@ export function AIChatIcon({
</g>
</svg>
);
}
export function AISearchIcon({
className = 'size-4',
state = 'default',
}: Pick<AIChatIconProps, 'className' | 'state'>) {
return (
<div
className={tcls(
'relative',
state === 'intro' && 'animate-[fadeIn_1s_both,orbit_1s_cubic-bezier(0.16,1,0.3,1)]',
state === 'thinking' || state === 'working'
? 'animate-[fadeIn_1s_both,orbit_1s_ease-out,orbit_2s_1s_infinite_forwards_linear]'
: ''
)}
>
<Icon icon="search" className={className} />
<Icon
icon="sparkle"
iconStyle={IconStyle.Solid}
className={tcls(
'absolute top-[15.7%] left-[15.6%] size-[50%]',
state === 'thinking' || state === 'working'
? 'animate-[spin_2s_infinite_forwards_cubic-bezier(0.16,1,0.3,1)]'
: '',
state === 'intro'
? 'animate-[spin_2s_.5s_forwards_cubic-bezier(0.16,1,0.3,1)]'
: ''
)}
/>
</div>
);
}
};
@@ -3,7 +3,6 @@ import { tcls } from '@/lib/tailwind';
import { Icon } from '@gitbook/icons';
import { useEffect, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useAIChatState } from '../AI/useAIChat';
import { Button, HoverCard, HoverCardRoot, HoverCardTrigger } from '../primitives';
import { KeyboardShortcut } from '../primitives/KeyboardShortcut';
@@ -20,7 +19,6 @@ export function AIChatInput(props: {
const { value, onChange, onSubmit, disabled, loading } = props;
const language = useLanguage();
const chat = useAIChatState();
const inputRef = useRef<HTMLTextAreaElement>(null);
@@ -34,7 +32,7 @@ export function AIChatInput(props: {
};
useEffect(() => {
if (chat.opened && !disabled && !loading) {
if (!disabled && !loading) {
// Add a small delay to ensure the input is rendered before focusing
// This fixes inconsistent focus behaviour across browsers
const timeout = setTimeout(() => {
@@ -43,7 +41,7 @@ export function AIChatInput(props: {
return () => clearTimeout(timeout);
}
}, [disabled, loading, chat.opened]);
}, [disabled, loading]);
useHotkeys(
'mod+i',
@@ -57,7 +55,7 @@ export function AIChatInput(props: {
);
return (
<div className="depth-subtle:has-[textarea:focus]:-translate-y-px relative flex animate-blur-in-slow flex-col overflow-hidden circular-corners:rounded-3xl rounded-corners:rounded-xl bg-tint-base/9 depth-subtle:shadow-sm shadow-tint/6 ring-1 ring-tint-subtle backdrop-blur-lg transition-all depth-subtle:has-[textarea:focus]:shadow-lg has-[textarea:focus]:shadow-primary-subtle has-[textarea:focus]:ring-2 has-[textarea:focus]:ring-primary-hover contrast-more:bg-tint-base dark:shadow-tint-1">
<div className="depth-subtle:has-[textarea:focus]:-translate-y-px relative flex flex-col overflow-hidden circular-corners:rounded-2xl rounded-corners:rounded-md bg-tint-base/9 depth-subtle:shadow-sm shadow-tint/6 ring-1 ring-tint-subtle backdrop-blur-lg transition-all depth-subtle:has-[textarea:focus]:shadow-lg has-[textarea:focus]:shadow-primary-subtle has-[textarea:focus]:ring-2 has-[textarea:focus]:ring-primary-hover contrast-more:bg-tint-base dark:shadow-tint-1">
<textarea
ref={inputRef}
disabled={disabled || loading}
@@ -108,7 +106,7 @@ export function AIChatInput(props: {
</div>
) : null}
<div className="absolute inset-x-0 bottom-0 flex items-center gap-2 px-2 py-2">
<HoverCardRoot openDelay={500}>
<HoverCardRoot>
<HoverCard
className="max-w-xs bg-tint p-2 text-sm text-tint"
arrow={{ className: 'fill-tint-3' }}

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