Compare commits

...

13 Commits

Author SHA1 Message Date
Peter White 384cd63729 changeset 2026-10-14 10:18:25 +02:00
Peter White 0898723168 Expose preview segment claims to expressions 2026-10-14 10:18:25 +02:00
Peter White f7dc7faa65 Reserve webframe size before hydration (#4683) 2026-10-14 08:25:57 +02:00
Peter White 03aa0f75f5 Render one shared Ask AI paragraph button (#4681) 2026-10-13 14:57:45 +02:00
Peter White a625112106 Skip the Ask AI paragraph button inside tables (#4680) 2026-10-13 13:40:20 +02:00
Brett Jephson 1bf3bd4294 Bump API version (#4679) 2026-10-13 10:55:32 +01:00
Peter White 9d62cb4bad Keep TOC scroll position when expanding a group (#4678) 2026-10-13 09:25:16 +02:00
Peter White 8bc381d852 Keep button labels readable inside colored hints (#4677) 2026-10-12 15:24:21 +02:00
Peter White a22923b56b Ask contributors to open issues instead of pull requests (#4676) 2026-10-12 12:06:51 +02:00
Peter White a73800e0ef Upgrade object-identity (#4675) 2026-10-12 11:22:25 +02:00
Steven H a0c1bdf58e Improve the Ask prompt for llms.txt (#4674) 2026-10-09 12:09:23 +01:00
Steven H d685660e1d Ensure we send the agent goal with the ask (#4673) 2026-10-08 20:51:24 +01:00
Peter White f1d5123c0b Support fr-ca, es-mx and es-419 site UI locales (RND-13260, RND-11280) (#4671) 2026-10-08 17:10:49 +02:00
37 changed files with 444 additions and 193 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Update API
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix table of contents scrolling back to the current page when expanding another group.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix low-contrast button labels inside success, warning and danger hints.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Agents can now send a goal with any question they ask on the Markdown version of a page.
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Show expression values in site previews that use a visitor segment.
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Show site UI in French or Spanish for spaces set to Canadian French, Mexican Spanish or Latin American Spanish.
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Render a single Ask AI paragraph button that moves to the hovered paragraph, instead of one hidden button per paragraph.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Skip rendering the Ask AI paragraph button inside table cells, where it is hidden, to keep pages with large tables within memory limits.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Upgrade object-identity to preserve value types when caching object arguments.
@@ -0,0 +1,6 @@
---
"@gitbook/react-contentkit": patch
"gitbook": patch
---
Reserve the webframe's size in the server-rendered layout so anchor links to content below integration blocks no longer land too high.
+46 -70
View File
@@ -1,113 +1,89 @@
# Welcome to GitBook's contributing guide!
# Contributing to GitBook
> _For help, support, feature requests, and product questions - head to our [GitHub Community](https://github.com/orgs/GitbookIO/discussions) 🤖_
> _For help, support and product questions, head to our [GitHub Community](https://github.com/orgs/GitbookIO/discussions)._
Thank you for investing your time in contributing to GitBook. Any contribution you make will be reviewed by our team. In this guide, you'll learn the different ways you can contribute.
Thanks for helping make GitBook better. This repository holds the open source renderer behind every published GitBook site, and the team reads everything that comes in here.
## Types of Contributions
## Ways to contribute
This repository contains code related to the rendering engine of GitBook's published content. Depending on what you'd like to contribute to, head to the section below to find the necessary steps.
- **Open an issue.** Report a bug, suggest a feature, or tell us when something feels off. This is the most useful thing you can do.
- **Join the conversation.** Share ideas and help others in [GitHub Community](https://github.com/orgs/GitbookIO/discussions).
- **Build with GitBook.** Publish your docs, try new features early, and tell us what works and what doesn't.
### Add a feature
## Issues instead of pull requests
Because this portion of GitBook is open source and available for you to use - if you think you can provide extra value through a new feature - you're welcome to add it! If you plan to distribute the code, keep the source code public to comply with GNU GPLv3. To clone in a private repository, you must first acquire a [commercial license](https://www.gitbook.com/pricing).
Pull requests are limited to the GitBook team. If you've found a bug or want something to change, please [open an issue](https://github.com/GitbookIO/gitbook/issues/new/choose) instead.
### Create a new issue
This isn't because we don't want your help. Every change here ships to every published GitBook site, so we need to own each one from start to finish. A clear issue often helps us more than a pull request: it tells us what's wrong and why, and lets us fix it in a way that fits the rest of the codebase.
If you spot a problem within a repository, [search if an issue already exists](https://docs.github.com/en/github/searching-for-information-on-github/searching-on-github/searching-issues-and-pull-requests#search-by-the-title-body-or-comments). If a related issue doesn't exist, you can open a new issue here!Please make sure any added issues are
A good issue includes:
- Descriptive
- Thoughtful
- Organized
- What you expected to happen, and what happened instead
- A link to a published GitBook site that shows the problem, if you have one
- Steps to reproduce it, plus screenshots or a recording for anything visual
- Your browser and OS, if it looks like it might matter
We recommend adding as many relevant links, minimal reproductions of the issue, and other materials that will help our team solve the issue fast.
Before opening one, [search existing issues](https://github.com/GitbookIO/gitbook/issues). If someone has already reported it, add a 👍 or any extra details you have.
### Solve an issue
Already fixed it in your fork? Link the branch or commit in your issue. We may open a pull request from it, and your commits keep your name.
If you're interested in solving an issue in our repository, start by scanning through it's exisiting issues to find one that you're interested in working on. If you find an issue to work on, you are welcome to open a PR with a fix. See the following sections below for more information on contributing for specific sections.
If you open a pull request anyway, we'll close it and point you here. Nothing personal, and we'd still love the issue.
### Documentation
## Reach out
The official documentation on GitBook open can be found directly in this Readme. Any updates or changes you would like to make, you can make directly to the README of this repository.
Not sure if it's a bug? Have a half-formed idea? Open an issue or start a [discussion](https://github.com/orgs/GitbookIO/discussions) anyway. We'd much rather hear from you than not, and no report is too small.
## Contributing
Spotted a wrong or missing translation? Open an issue with the string and the wording you'd suggest.
### Make changes locally
## Using AI
Any contribution you make can be made to the code located in this repository. In order to contribute, you'll need to start off of a local version of this repository.
You're welcome to use AI to help you write an issue. Please read and check what it produces before you post: reports that are long, vague or unverified take time away from fixing real problems, and we may close them without a reply.
#### 1. Fork the repository
## Running locally
##### Using GitHub Desktop:
You don't need a local setup to open an issue, but it can help you reproduce a problem or self-host the renderer.
- [Getting started with GitHub Desktop](https://docs.github.com/en/desktop/installing-and-configuring-github-desktop/getting-started-with-github-desktop) will guide you through setting up Desktop.
- Once Desktop is set up, you can use it to [fork the repo](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/cloning-and-forking-repositories-from-github-desktop)!
### Prerequisites
##### Using the command line:
- [Node.js](https://nodejs.org/en) (see "engines" in `package.json`)
- [Bun](https://bun.sh/) (see "packageManager" in `package.json`)
- [Fork the repo](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#fork-an-example-repository) so that you can make your changes without affecting the original project until you're ready to merge them.
### Setup
#### 2. Create a working branch and start with your changes
1. Use the project's version of Node:
After forking this repository, you'll want to [create a branch](https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-a-branch-for-an-issue) to work off of.
#### 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`)
##### Setup steps:
1. Ensure you are using the project's version of Node:
```bash
nvm use
```
2. Install dependencies using Bun:
2. Install dependencies:
```bash
bun install
```
3. Start the development server:
```bash
bun dev
```
Additional development commands:
- `bun lint`: Lint the code using Oxlint
- `bun lint:fix`: Automatically fix lint issues using Oxlint
- `bun format`: Format the code using Oxfmt
- `bun format:check`: Check formatting without changing files
- `bun typecheck`: Run TypeScript type checking
- `bun unit`: Run unit tests
- `bun e2e`: Run end-to-end tests
Other commands:
#### 4. Preview your changes
- `bun lint`: Lint the code using Oxlint
- `bun lint:fix`: Automatically fix lint issues using Oxlint
- `bun format`: Format the code using Oxfmt
- `bun format:check`: Check formatting without changing files
- `bun typecheck`: Run TypeScript type checking
- `bun unit`: Run unit tests
- `bun e2e`: Run end-to-end tests
When running the development server, published GitBook sites can be rendered through your local version at `http://localhost:3000/url`.
### Preview a site
For example, our published docs can be viewed using the local version by visiting `http://localhost:3000/url/gitbook.com/docs` after running the development server.
With the development server running, any published GitBook site can be rendered through your local version at `http://localhost:3000/url/<published-url>`. For example, visit `http://localhost:3000/url/gitbook.com/docs` to see our own docs.
You can visit any published GitBook site behind your development server. Please make sure your site is [published publicly](https://gitbook.com/docs/published-documentation/publish-your-content-as-a-docs-site) to ensure you can view the site correctly in your development version.
The site must be [published publicly](https://gitbook.com/docs/published-documentation/publish-your-content-as-a-docs-site) to render locally.
### Commit your update
## License
[Commit your changes](https://github.com/git-guides/git-commit) once you are happy with them. See [Atom's contributing guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md#git-commit-messages) to know how to use emoji for commit messages!
Once your changes are ready, don't forget to self-review your code to double check that your chagnes are ready to be added.
### Pull Request
When you're finished with the changes, [create a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request), also known as a PR.
- Don't forget to [link PR to issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) if you are solving one.
- Enable the checkbox to [allow maintainer edits](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork) so the branch can be updated for a merge. Once you submit your PR, a GitBook team member will review your proposal. We may ask questions or request for additional information.
- We may ask for changes to be made before a PR can be merged, either using [suggested changes](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/incorporating-feedback-in-your-pull-request) or pull request comments. You can apply suggested changes directly through the UI. You can make any other changes in your fork, then commit them to your branch.
- As you update your PR and apply changes, mark each conversation as [resolved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request#resolving-conversations).
- If you run into any merge issues, checkout this [git tutorial](https://lab.github.com/githubtraining/managing-merge-conflicts) to help you resolve merge conflicts and other issues.
### Your PR is merged
Congratulations 🎉 Thank you for your contribution! Once your PR is merged, your contributions will be publicly visible on the relevant repository.
The renderer is distributed under the [GNU GPLv3 License](https://github.com/GitbookIO/gitbook/blob/main/LICENSE). If you distribute the code, you must keep the source code public. To use it in a private repository, you need a [commercial license](https://www.gitbook.com/pricing).
+5 -5
View File
@@ -85,7 +85,7 @@ GitBook Open uses fontawesome. For self-hosting and local development, for licen
## Contributing
GitBook's rendering engine is fully open source and built on top of [Next.js](https://nextjs.org/). Head to our [contributing guide](https://github.com/GitbookIO/gitbook/blob/main/.github/CONTRIBUTING.md) to learn more about the workflow on adding your first Pull Request.
GitBook's rendering engine is fully open source and built on top of [Next.js](https://nextjs.org/). Head to our [contributing guide](https://github.com/GitbookIO/gitbook/blob/main/.github/CONTRIBUTING.md) to learn how to report bugs and suggest changes.
### Types of contributions
@@ -93,22 +93,22 @@ We encourage you to contribute to GitBook to help us build the best tool for doc
#### Translations
The GitBook UI is rendered using a set of translation files found in [`packages/gitbook/src/intl/translations`](/packages/gitbook/src/intl/translations/). We welcome all additional translations for the UI.
The GitBook UI is rendered using a set of translation files found in [`packages/gitbook/src/intl/translations`](/packages/gitbook/src/intl/translations/). Spotted a wrong or missing translation? Open an issue and let us know.
#### Bugs
Encounter a bug or find an issue you'd like to fix? Helping us fix issues related to GitBook greatly improves the experience for everyone. Head to the issues section of this repository to learn more about the types of bugs you can already help out with.
Encounter a bug? [Open an issue](https://github.com/GitbookIO/gitbook/issues/new/choose) with as much detail as you can. Clear reports help us fix things faster for everyone.
## Deployment
> [!WARNING]
> While it is possible to self-host this project, we do not recommend this unless you are certain this option fits your need.
>
> _Looking to add a specific feature in GitBook? Head to our [contributing guide](https://github.com/GitbookIO/gitbook/blob/main/.github/CONTRIBUTING.md) to get started._
> _Looking for a specific feature in GitBook? Head to our [contributing guide](https://github.com/GitbookIO/gitbook/blob/main/.github/CONTRIBUTING.md) to suggest it._
>
> Self-hosting this project puts the responsibility of maintaining and merging future updates on **you**. We cannot guarantee support, maintenance, or updates to forked and self-hosted instances of this project.
>
> We want to make it as easy as possible for our community to collaborate and push the future of GitBook, which is why we encourage you to contribute to our product directly instead of creating your own version.
> We want to make it as easy as possible for our community to collaborate and push the future of GitBook, which is why we encourage you to share your ideas with us instead of creating your own version.
This project allows you to self-host the rendering portion of your GitBook published content. Self-hosting has pros and cons.
+4 -6
View File
@@ -164,7 +164,7 @@
"next-themes": "^0.4.6",
"nuqs": "^2.2.3",
"object-hash": "^3.0.0",
"object-identity": "^0.1.2",
"object-identity": "^0.2.1",
"openapi-types": "^12.1.3",
"p-map": "^7.0.3",
"p-retry": "^8.0.0",
@@ -355,7 +355,7 @@
},
"catalog": {
"@base-ui/react": "^1.7.0",
"@gitbook/api": "0.205.0",
"@gitbook/api": "0.206.0",
"@scalar/api-client-react": "^1.3.46",
"@tsconfig/node20": "^20.1.6",
"@tsconfig/strictest": "^2.0.6",
@@ -727,7 +727,7 @@
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.2.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.2.0" } }, "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q=="],
"@gitbook/api": ["@gitbook/api@0.205.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-jjkPAhPw6tja1N0ZPKqVsaV+opN8IS7yP1oZHHbcacZ477angPCo9YqiJJYWAl0tQ5OcKTuQbrsqhZSuwKM2Kw=="],
"@gitbook/api": ["@gitbook/api@0.206.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-ctgnDSQ26ZeZvdU/Zb1xxU97yMquKnhGiA0QblAIzKUmfi/3aYmWYHAwXwvEg0wtBrEtrjzme9pXgX/ACWnANA=="],
"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],
@@ -2677,7 +2677,7 @@
"object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="],
"object-identity": ["object-identity@0.1.2", "", {}, "sha512-Px5puVllX5L2aBjbcfXpiG5xXeq6OE8RckryTeP2Zq+0PgYrCGJXmC6LblWgknKSJs11Je2W4U2NOWFj3t/QXQ=="],
"object-identity": ["object-identity@0.2.1", "", {}, "sha512-itX2N4m0IvaLhFZMi9YkW4nFML6FshKCCpnn6iKJZVIkCNM/ZRaG4t1bzD2ncaW4r6cHJa59XY+7oZAH7tg8tA=="],
"object-inspect": ["object-inspect@1.13.2", "", {}, "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g=="],
@@ -2933,8 +2933,6 @@
"rolldown-plugin-dts": ["rolldown-plugin-dts@0.16.11", "", { "dependencies": { "@babel/generator": "^7.28.3", "@babel/parser": "^7.28.4", "@babel/types": "^7.28.4", "ast-kit": "^2.1.2", "birpc": "^2.6.1", "debug": "^4.4.3", "dts-resolver": "^2.1.2", "get-tsconfig": "^4.10.1", "magic-string": "^0.30.19" }, "peerDependencies": { "@ts-macro/tsc": "^0.3.6", "@typescript/native-preview": ">=7.0.0-dev.20250601.1", "rolldown": "^1.0.0-beta.9", "typescript": "^5.0.0", "vue-tsc": "~3.1.0" }, "optionalPeers": ["@ts-macro/tsc", "@typescript/native-preview", "typescript", "vue-tsc"] }, "sha512-9IQDaPvPqTx3RjG2eQCK5GYZITo203BxKunGI80AGYicu1ySFTUyugicAaTZWRzFWh9DSnzkgNeMNbDWBbSs0w=="],
"rollup": ["rollup@3.29.5", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w=="],
"roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
"router": ["router@2.0.0", "", { "dependencies": { "array-flatten": "3.0.0", "is-promise": "4.0.0", "methods": "~1.1.2", "parseurl": "~1.3.3", "path-to-regexp": "^8.0.0", "setprototypeof": "1.2.0", "utils-merge": "1.0.1" } }, "sha512-dIM5zVoG8xhC6rnSN8uoAgFARwTE7BQs8YwHEvK0VCmfxQXMaOuA1uiR1IPwsW7JyK5iTt7Od/TC9StasS2NPQ=="],
+1 -1
View File
@@ -48,7 +48,7 @@
"@tsconfig/strictest": "^2.0.6",
"@tsconfig/node20": "^20.1.6",
"@base-ui/react": "^1.7.0",
"@gitbook/api": "0.205.0",
"@gitbook/api": "0.206.0",
"@scalar/api-client-react": "^1.3.46",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
+1 -1
View File
@@ -55,7 +55,7 @@
"next-themes": "^0.4.6",
"nuqs": "^2.2.3",
"object-hash": "^3.0.0",
"object-identity": "^0.1.2",
"object-identity": "^0.2.1",
"openapi-types": "^12.1.3",
"p-map": "^7.0.3",
"p-retry": "^8.0.0",
@@ -1,29 +1,58 @@
'use client';
import fnv1a from '@sindresorhus/fnv1a';
import * as React from 'react';
import { createPortal } from 'react-dom';
import { AIChatIcon } from '../AIChatIcon';
import { useAIChatController, useAIConfig } from '@/components/AI';
import { Button } from '@/components/primitives';
import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled';
import { t, tString, useLanguage } from '@/intl/client';
import { type ClassValue, tcls } from '@/lib/tailwind';
import { tcls } from '@/lib/tailwind';
/**
* A small icon button revealed in the left margin of a top-level paragraph when it is hovered (on
* devices with a fine pointer). Clicking it stages the paragraph's text as a reference and opens
* the AI chat — a more discoverable variant of the text-selection "Ask" affordance.
* A small icon button revealed in the left margin of a paragraph when it is hovered (on devices
* with a fine pointer). Clicking it stages the paragraph's text as a reference and opens the AI
* chat — a more discoverable variant of the text-selection "Ask" affordance.
*
* The button is absolutely positioned so it never affects the document flow, and its visibility is
* driven purely by the `group/ask-ai` hover state of its paragraph wrapper.
* A single instance is portaled into the last hovered paragraph wrapper, rather than rendering one
* per paragraph, so pages with many paragraphs don't pay for markup that is hidden until hovered.
* Its visibility is still driven purely by the `group/ask-ai` hover state of that wrapper.
*/
export function AskAIParagraphButton(props: { content: string; className?: ClassValue }) {
const { content, className } = props;
export function AskAIParagraphButton() {
const config = useAIConfig();
const language = useLanguage();
const chatController = useAIChatController();
const [target, setTarget] = React.useState<HTMLElement | null>(null);
const enabled = isAIChatEnabled(config.aiMode);
React.useEffect(() => {
if (!enabled) {
return;
}
const onPointerOver = (event: PointerEvent) => {
if (!(event.target instanceof Element)) {
return;
}
const wrapper = event.target.closest<HTMLElement>('[data-ask-ai-paragraph]');
if (wrapper) {
setTarget(wrapper);
}
};
document.addEventListener('pointerover', onPointerOver);
return () => document.removeEventListener('pointerover', onPointerOver);
}, [enabled]);
if (!enabled || !target) {
return null;
}
const onClick = () => {
const text = content.trim();
const text = getParagraphText(target).trim();
if (!text) {
return;
}
@@ -38,7 +67,7 @@ export function AskAIParagraphButton(props: { content: string; className?: Class
chatController.focus();
};
return (
return createPortal(
<div
className={tcls(
// Sit in the left margin, flush against the paragraph so there is no hover gap.
@@ -52,13 +81,7 @@ export function AskAIParagraphButton(props: { content: string; className?: Class
'invisible opacity-0 transition-opacity duration-150',
'hover:visible hover:opacity-100 group-hover/ask-ai:visible group-hover/ask-ai:opacity-100',
// Never shown on touch / hover-less contexts.
'not-pointer-fine:hidden',
// Hidden where an overflow-clipped ancestor would cut it off (ARIA and native
// tables, record cards).
'in-[[role=table]]:hidden',
'in-[table]:hidden',
'in-[[data-card]]:hidden',
className
'not-pointer-fine:hidden'
)}
>
<Button
@@ -75,6 +98,37 @@ export function AskAIParagraphButton(props: { content: string; className?: Class
'in-[.hint.bg-danger]:bg-danger in-[.hint.bg-info]:bg-info in-[.hint.bg-success]:bg-success in-[.hint.bg-warning]:bg-warning'
)}
/>
</div>
</div>,
target
);
}
/**
* Read the paragraph's text, skipping rendered math like `getNodeText` does.
*/
function getParagraphText(wrapper: HTMLElement): string {
const paragraph = wrapper.querySelector(':scope > p');
if (!paragraph) {
return '';
}
const walker = document.createTreeWalker(
paragraph,
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
{
acceptNode: (node) =>
node instanceof Element &&
node.matches('.katex, mjx-container, [aria-hidden="true"]')
? NodeFilter.FILTER_REJECT
: NodeFilter.FILTER_ACCEPT,
}
);
let text = '';
while (walker.nextNode()) {
if (walker.currentNode.nodeType === Node.TEXT_NODE) {
text += walker.currentNode.textContent ?? '';
}
}
return text;
}
@@ -135,10 +135,10 @@ const HINT_STYLES: {
header: 'bg-warning-active',
body: [
'text-neutral-strong',
'links-default:[&_a]:text-warning',
'links-default:[&_a:hover]:text-warning-strong',
'links-default:[&_a]:decoration-warning/6',
'links-accent:[&_a]:decoration-warning',
'links-default:[&_a:not(.button)]:text-warning',
'links-default:[&_a:not(.button):hover]:text-warning-strong',
'links-default:[&_a:not(.button)]:decoration-warning/6',
'links-accent:[&_a:not(.button)]:decoration-warning',
'decoration-warning/6',
'[&_.can-override-bg]:bg-warning-active',
'[&_.can-override-text]:text-warning-strong',
@@ -152,10 +152,10 @@ const HINT_STYLES: {
header: 'bg-danger-active',
body: [
'text-neutral-strong',
'links-default:[&_a]:text-danger',
'links-default:[&_a:hover]:text-danger-strong',
'links-default:[&_a]:decoration-danger/6',
'links-accent:[&_a]:decoration-danger',
'links-default:[&_a:not(.button)]:text-danger',
'links-default:[&_a:not(.button):hover]:text-danger-strong',
'links-default:[&_a:not(.button)]:decoration-danger/6',
'links-accent:[&_a:not(.button)]:decoration-danger',
'decoration-danger/6',
'[&_.can-override-bg]:bg-danger-active',
'[&_.can-override-text]:text-danger-strong',
@@ -169,10 +169,10 @@ const HINT_STYLES: {
header: 'bg-success-active',
body: [
'text-neutral-strong',
'links-default:[&_a]:text-success',
'links-default:[&_a:hover]:text-success-strong',
'links-default:[&_a]:decoration-success/6',
'links-accent:[&_a]:decoration-success',
'links-default:[&_a:not(.button)]:text-success',
'links-default:[&_a:not(.button):hover]:text-success-strong',
'links-default:[&_a:not(.button)]:decoration-success/6',
'links-accent:[&_a:not(.button)]:decoration-success',
'decoration-success/6',
'[&_.can-override-bg]:bg-success-active',
'[&_.can-override-text]:text-success-strong',
@@ -52,6 +52,14 @@
@apply relative;
}
.contentkit-webframe-container {
@apply relative;
}
.contentkit-webframe-loading {
@apply absolute inset-0 m-auto size-5 animate-spin;
}
/** Card */
.contentkit-card {
@apply flex flex-col border rounded border-tint-subtle;
@@ -4,12 +4,11 @@ import { CustomizationAIMode } from '@gitbook/api';
import type { BlockProps } from './Block';
import { Inlines } from './Inlines';
import { getTextAlignment } from './utils';
import { AskAIParagraphButton } from '@/components/AIChat/AskAIParagraphButton';
import { getNodeText } from '@/lib/document';
import { tcls } from '@/lib/tailwind';
export function Paragraph(props: BlockProps<DocumentBlockParagraph>) {
const { block, style, ...contextProps } = props;
const { block, style, ancestorBlocks, ...contextProps } = props;
const { context } = contextProps;
const paragraph = (
@@ -28,6 +27,8 @@ export function Paragraph(props: BlockProps<DocumentBlockParagraph>) {
const contentContext = context.contentContext;
const aiAssistantEnabled =
context.mode !== 'print' &&
// Table cells would clip the margin button, and big tables hold thousands of paragraphs.
!ancestorBlocks.some((ancestor) => ancestor.type === 'table') &&
contentContext != null &&
'customization' in contentContext &&
contentContext.customization.ai.mode === CustomizationAIMode.Assistant;
@@ -38,6 +39,8 @@ export function Paragraph(props: BlockProps<DocumentBlockParagraph>) {
// `self-center`/`self-end`) — otherwise centered paragraphs pin left on wide/no-TOC pages.
return (
<div
// The shared `AskAIParagraphButton` portals itself into the hovered wrapper.
data-ask-ai-paragraph=""
className={tcls(
'group/ask-ai relative',
style,
@@ -45,7 +48,6 @@ export function Paragraph(props: BlockProps<DocumentBlockParagraph>) {
)}
>
{paragraph}
<AskAIParagraphButton content={text} />
</div>
);
}
@@ -50,6 +50,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
tag: Tag = 'div',
ariaLabelledBy,
block,
ancestorBlocks,
document,
record,
column,
@@ -151,7 +152,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
<Blocks
tag={Tag}
document={document}
ancestorBlocks={[]}
ancestorBlocks={[...ancestorBlocks, block]}
nodes={fragment.nodes}
style={[
'blocks',
@@ -1,6 +1,6 @@
import { expect, it } from 'bun:test';
import type { DocumentBlockParagraph, JSONDocument } from '@gitbook/api';
import type { DocumentBlockParagraph, DocumentBlockTable, JSONDocument } from '@gitbook/api';
import { isBlockOffscreen } from './isBlockOffscreen';
@@ -42,3 +42,31 @@ it('should return true if block is offscreen', () => {
)
).toBe(true);
});
it('should treat content in table cells as offscreen', () => {
const cell: DocumentBlockParagraph = {
object: 'block',
type: 'paragraph',
nodes: [],
};
const table: DocumentBlockTable = {
object: 'block',
type: 'table',
isVoid: true,
data: {
view: { type: 'cards', cardSize: 'medium', columns: ['column'] },
records: {},
definition: {},
},
fragments: [{ object: 'fragment', fragment: 'cell', nodes: [cell] }],
};
const document: JSONDocument = {
object: 'document',
nodes: [table],
data: {},
};
expect(isBlockOffscreen({ document, block: cell, ancestorBlocks: [table] })).toBe(true);
});
@@ -18,6 +18,11 @@ export function isBlockOffscreen(
},
screenHeight = 500
): boolean {
// Cell content lives in fragments, not table nodes, so its row position can't be estimated.
if (ancestorBlocks.some((ancestor) => ancestor.type === 'table')) {
return true;
}
let current = block;
const allAncestors = [document, ...ancestorBlocks];
@@ -1,6 +1,7 @@
import type { CustomizationDefaultThemeMode } from '@gitbook/api';
import { SiteInsightsTrademarkPlacement } from '@gitbook/api';
import { AskAIParagraphButton } from '../AIChat';
import { LoadIntegrations } from '../Integrations';
import { NavigationLoader } from '../primitives/NavigationLoader';
import { SpaceLayoutServerContext } from '../SpaceLayout';
@@ -77,6 +78,7 @@ export async function EmbeddableRootLayout({
}}
>
<NavigationLoader />
<AskAIParagraphButton />
<VisibilityProvider className="fixed inset-0 flex flex-col">
{children}
{context.customization.trademark.enabled ? (
@@ -5,7 +5,7 @@ import { CustomizationHeaderPreset, CustomizationSearchStyle } from '@gitbook/ap
import { AdaptiveVisitorContextProvider } from '../Adaptive';
import { AIChatProvider } from '../AI';
import type { RenderAIMessageOptions } from '../AI';
import { AIChat, AskAITextSelection } from '../AIChat';
import { AIChat, AskAIParagraphButton, AskAITextSelection } from '../AIChat';
import { Announcement } from '../Announcement';
import { SpacesDropdown, TranslationsDropdown } from '../Header/SpacesDropdown';
import { CurrentContentProvider } from '../hooks';
@@ -138,6 +138,7 @@ export function SpaceLayout(props: SpaceLayoutProps) {
<>
<AIChat />
<AskAITextSelection />
<AskAIParagraphButton />
</>
) : null}
@@ -100,11 +100,19 @@ export function ScrollContainer(props: ScrollContainerProps) {
}
let frame = 0;
let previousActiveItems = new Set(container.querySelectorAll(active));
// Active items can mount only after a collapsed group expands.
const observer = new MutationObserver(() => {
cancelAnimationFrame(frame);
frame = requestAnimationFrame(() => {
for (const activeItem of container.querySelectorAll(active)) {
const activeItems = new Set(container.querySelectorAll(active));
const newActiveItems = [...activeItems].filter(
(item) => !previousActiveItems.has(item)
);
previousActiveItems = activeItems;
// Only follow newly active items so unrelated changes (e.g. expanding another
// group) don't pull the user back to the current page.
for (const activeItem of newActiveItems) {
if (!isElementFullyVisibleInContainer(activeItem, container)) {
scrollToElementInContainer(activeItem, container, 'smooth');
return;
@@ -0,0 +1,20 @@
import { describe, expect, it } from 'bun:test';
import { es } from './es';
import { fr } from './fr';
import { isAvailableLanguage, languages, loadLanguage } from './index';
describe('regional language variants', () => {
it.each([
['fr-ca', fr],
['es-mx', es],
['es-419', es],
] as const)('loads %s with its own metadata and the base strings', async (locale, base) => {
expect(isAvailableLanguage(locale)).toBe(true);
const language = await loadLanguage(locale);
expect(language).toEqual({ ...base, ...languages[locale] });
expect(language.locale).toBe(locale);
});
});
@@ -25,6 +25,10 @@ const languageDefinitions = {
metadata: { locale: 'fr', language: 'Français', flag: '🇫🇷' },
load: () => import('./fr').then((module) => module.fr),
},
'fr-ca': {
metadata: { locale: 'fr-ca', language: 'Français (Canada)', flag: '🇨🇦' },
load: () => import('./fr').then((module) => withMetadata(module.fr, 'fr-ca')),
},
de: {
metadata: { locale: 'de', language: 'Deutsch', flag: '🇩🇪' },
load: () => import('./de').then((module) => module.de),
@@ -33,6 +37,14 @@ const languageDefinitions = {
metadata: { locale: 'es', language: 'Español', flag: '🇪🇸' },
load: () => import('./es').then((module) => module.es),
},
'es-mx': {
metadata: { locale: 'es-mx', language: 'Español (México)', flag: '🇲🇽' },
load: () => import('./es').then((module) => withMetadata(module.es, 'es-mx')),
},
'es-419': {
metadata: { locale: 'es-419', language: 'Español (Latinoamérica)', flag: '🌎' },
load: () => import('./es').then((module) => withMetadata(module.es, 'es-419')),
},
it: {
metadata: { locale: 'it', language: 'Italiano', flag: '🇮🇹' },
load: () => import('./it').then((module) => module.it),
@@ -177,6 +189,14 @@ const languageDefinitions = {
export type TranslationLocale = keyof typeof languageDefinitions;
// Regional variants reuse their base language's strings but keep their own locale, name and flag.
function withMetadata(
base: TranslationLanguage,
locale: 'fr-ca' | 'es-mx' | 'es-419'
): TranslationLanguage {
return { ...base, ...languageDefinitions[locale].metadata };
}
export const languages = Object.fromEntries(
Object.entries(languageDefinitions).map(([locale, definition]) => [locale, definition.metadata])
) as {
+43 -8
View File
@@ -1,10 +1,6 @@
/**
* Describe the `ask` and `goal` query parameters of the ask endpoint, for agent-facing prompts.
*/
export function renderAskParametersDescription(): string {
return `\`ask\` is the immediate question: it should be specific, self-contained, and written in natural language.
\`goal\` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with \`ask=how do I create an API token\`, a goal like \`build a script that syncs our docs to a CMS\` lets GitBook tailor the answer to that use case.`;
}
import { isAIEnabled } from '@/components/utils/isAIChatEnabled';
import type { GitBookSiteContext } from '@/lib/context';
import { linkerWithMarkdownPages } from '@/lib/links';
/**
* Render the "Querying This Documentation" section of the agent instructions.
@@ -18,7 +14,46 @@ export function renderQueryingDocumentation(options: { pageUrl: string }): strin
GET ${pageUrl}?ask=<question>&goal=<user_goal>
\`\`\`
${renderAskParametersDescription()}
\`ask\` is the immediate question: it should be specific, self-contained, and written in natural language.
\`goal\` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with \`ask=how do I create an API token\`, a goal like \`automate deployments from our CI pipeline\` lets GitBook tailor the answer to that use case.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.`;
}
const AGENT_INSTRUCTIONS_INTRO = `# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.
## Querying This Documentation`;
/**
* Render the "Agent Instructions" of a markdown page (callers add the separator).
*/
export function renderPageAgentInstructions(options: { pageUrl: string }): string {
return `${AGENT_INSTRUCTIONS_INTRO}
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.
${renderQueryingDocumentation(options)}
Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
`;
}
/**
* Render the "Agent Instructions" of the site-wide llms.txt and llms-full.txt files.
* Returns `undefined` when AI is disabled for the site.
*/
export function renderSiteAgentInstructions(context: GitBookSiteContext): string | undefined {
if (!isAIEnabled(context.customization.ai.mode)) {
return undefined;
}
// The ask endpoint ignores the page path, so the top-level `index.md` works on every site.
const linker = linkerWithMarkdownPages(context.linker);
const pageUrl = linker.toAbsoluteURL(linker.toPathForPagePath({ path: 'index' }));
return `${AGENT_INSTRUCTIONS_INTRO}
This site has an agentic ask interface you may use to query the documentation dynamically by asking a question.
${renderQueryingDocumentation({ pageUrl })}
`;
}
+11
View File
@@ -67,6 +67,17 @@ describe('withStableRef', () => {
expect(ref1).not.toBe(ref2);
});
it('should preserve value types in object references', () => {
const toStableRef = withStableRef();
const numeric = { id: 1 };
const textual = { id: '1' };
expect(toStableRef(numeric)).toBe(numeric);
expect(toStableRef(textual)).toBe(textual);
expect(toStableRef({ id: 1 })).toBe(numeric);
expect(toStableRef({ id: '1' })).toBe(textual);
});
it('should maintain reference stability across multiple calls', () => {
const toStableRef = withStableRef();
+4 -4
View File
@@ -91,7 +91,7 @@ export function hasMoreThan(
if (traverse(child)) return true;
}
if (child.object === 'block' && 'fragments' in child) {
if (child.object === 'block' && 'fragments' in child && child.fragments) {
for (const fragment of child.fragments) {
if (traverse(fragment)) return true;
}
@@ -159,7 +159,7 @@ export function getNodeFragmentByType(
if (!('fragments' in node)) {
return null;
}
const fragment = node.fragments.find((child: any) => child.type === type);
const fragment = node.fragments?.find((child: any) => child.type === type);
return fragment ?? null;
}
@@ -301,7 +301,7 @@ function findBlocks<Block extends DocumentBlock>(
}
}
if (block.object === 'block' && 'fragments' in block) {
if (block.object === 'block' && 'fragments' in block && block.fragments) {
for (const fragment of block.fragments) {
const result = findBlocks<Block>(fragment, test);
if (result.length > 0) {
@@ -341,7 +341,7 @@ function findBlock(
}
}
if (block.object === 'block' && 'fragments' in block) {
if (block.object === 'block' && 'fragments' in block && block.fragments) {
for (const fragment of block.fragments) {
const result = findBlock(fragment, test);
if (result) {
+26
View File
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'bun:test';
import type { JwtPayload } from 'jwt-decode';
import { NextRequest } from 'next/server';
import {
type ResponseCookies,
@@ -14,6 +15,7 @@ import {
isRevalidationRequest,
normalizeVisitorURL,
parseHostnameList,
serveVisitorClaimsDataRequest,
shouldSendVisitorCountry,
} from './visitors';
@@ -691,3 +693,27 @@ describe('parseHostnameList', () => {
);
});
});
describe('serveVisitorClaimsDataRequest', () => {
it('should expose the claims of the preview API token', async () => {
const siteRequestURL = new URL(
'https://sites.gitbook.com/preview/site_foo/~gitbook/visitor'
);
const claims = { apiUrl: 'https://client-123.example.com', isBeta: true };
const previewAPIToken = [
btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' })),
btoa(JSON.stringify({ kind: 'site', site: 'site_foo', claims })),
'signature',
].join('.');
const response = serveVisitorClaimsDataRequest(
new NextRequest(siteRequestURL),
siteRequestURL,
previewAPIToken
);
expect(await response.json()).toEqual({
visitor: { claims: { unsigned: {}, ...claims } },
});
});
});
+16 -2
View File
@@ -3,7 +3,7 @@ import { type JwtPayload, jwtDecode } from 'jwt-decode';
import { type NextRequest, NextResponse } from 'next/server';
import hash from 'object-hash';
import type { SiteVisitorPayload } from '@gitbook/api';
import type { SiteAPIToken, SiteVisitorPayload } from '@gitbook/api';
import {
MAX_CHUNKED_COOKIE_LENGTH,
@@ -571,7 +571,21 @@ export function getVisitorAuthCookieMaxAge(decoded: JwtPayload): number {
/**
* Handler for the /~gitbook/visitor middleware route to expose visitor data.
*/
export function serveVisitorClaimsDataRequest(request: NextRequest, siteRequestURL: URL) {
export function serveVisitorClaimsDataRequest(
request: NextRequest,
siteRequestURL: URL,
previewAPIToken?: string
) {
// A site preview carries the visitor segment claims in its API token, not in a visitor token.
if (previewAPIToken) {
try {
const { claims } = jwtDecode<SiteAPIToken>(previewAPIToken);
return NextResponse.json({ visitor: { claims: { unsigned: {}, ...claims } } });
} catch (error) {
console.warn('Error decoding preview API token', error);
}
}
const { visitorToken, unsignedClaims } = getVisitorData({
cookies: request.cookies.getAll(),
headers: request.headers,
+18 -3
View File
@@ -210,7 +210,13 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
// Handler that returns visitor data for the app to consume.
if (siteRequestURL.pathname.endsWith('/~gitbook/visitor')) {
return serveVisitorClaimsDataRequest(request, siteRequestURL);
const previewAPIToken = isPreviewRequest(siteRequestURL)
? getAPITokenFromCookies(
request.cookies.getAll(),
getAPITokenCookieName(getPreviewScopePath(siteRequestURL))
)
: undefined;
return serveVisitorClaimsDataRequest(request, siteRequestURL, previewAPIToken);
}
//
@@ -625,7 +631,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
request.headers.set('x-gitbook-disable-tracking', 'true');
return serveWithQueryAPIToken({
// We scope the API token to the site ID.
scopePath: ['preview', getPreviewRequestIdentifier(siteRequestURL)].join('/'),
scopePath: getPreviewScopePath(siteRequestURL),
// We keep the original request URL when using `url` mode
requestURL: mode === 'url' ? requestURL : siteRequestURL,
requestCookies: request.cookies,
@@ -677,6 +683,14 @@ function serveErrorResponse(error: Error) {
throw error;
}
function getPreviewScopePath(siteRequestURL: URL) {
return ['preview', getPreviewRequestIdentifier(siteRequestURL)].join('/');
}
function getAPITokenCookieName(scopePath: string) {
return getPathScopedCookieName('gitbook-api-token', scopePath);
}
/**
* Serve a response with an API token obtained from the query params.
*/
@@ -689,7 +703,7 @@ async function serveWithQueryAPIToken(input: {
const { scopePath, requestURL, requestCookies, serve } = input;
// We store the API token in a cookie that is scoped to the specific route
// to avoid errors when multiple previews are opened in different tabs.
const cookieName = getPathScopedCookieName('gitbook-api-token', scopePath);
const cookieName = getAPITokenCookieName(scopePath);
// Extract a potential GitBook API token passed in the request
// If found, we redirect to the same URL but with the token in the cookie
@@ -954,6 +968,7 @@ function encodePathInSiteContent(
{
type: 'ask_question',
query: ask,
goal: typeof goal === 'string' ? goal : undefined,
location: {
displayContext: SiteInsightsDisplayContext.Server,
},
+7
View File
@@ -4,6 +4,7 @@ import pMap, { pMapIterable } from 'p-map';
import type { RevisionPageDocument, SiteSection, SiteSpace } from '@gitbook/api';
import { renderSiteAgentInstructions } from '@/lib/ask-prompt';
import {
type GitBookSiteContext,
checkIsRootSiteContext,
@@ -171,6 +172,12 @@ async function streamMarkdownPageEntries(
stream.enqueue(new TextEncoder().encode(`${header.join('\n')}\n\n`));
}
// Rendered up front, on every part, since agents may only read the start of the file.
const agentInstructions = renderSiteAgentInstructions(context);
if (agentInstructions) {
stream.enqueue(new TextEncoder().encode(`${agentInstructions}\n---\n\n`));
}
// Process the pages
for await (const markdown of pMapIterable(
pagesToProcess,
+5 -27
View File
@@ -4,12 +4,11 @@ import { toMarkdown } from 'mdast-util-to-markdown';
import type { SiteSection, SiteSpace } from '@gitbook/api';
import { isAIEnabled } from '@/components/utils/isAIChatEnabled';
import { renderSiteAgentInstructions } from '@/lib/ask-prompt';
import { type GitBookSiteContext, checkIsRootSiteContext } from '@/lib/context';
import { throwIfDataError } from '@/lib/data';
import { type GitBookLinker, linkerWithMarkdownPages } from '@/lib/links';
import { getMarkdownContentType } from '@/lib/markdown-content-type';
import { resolveFirstDocument } from '@/lib/pages';
import { type FlatPageEntry, getIndexablePages } from '@/lib/sitemap';
import {
filterSiteSpacesByLocale,
@@ -49,7 +48,10 @@ export async function serveLLMsTxt(baseContext: GitBookSiteContext) {
bullet: '-',
});
output += renderAskFooter(context);
const agentInstructions = renderSiteAgentInstructions(context);
if (agentInstructions) {
output += `\n\n---\n\n${agentInstructions}`;
}
return new Response(output, {
headers: {
@@ -205,27 +207,3 @@ export async function getMarkdownForPagesTree(
});
return nodes;
}
function renderAskFooter(context: GitBookSiteContext) {
if (!isAIEnabled(context.customization.ai.mode)) {
return '';
}
return `\n\n---\n\n# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.
## Querying This Documentation
If you need additional information, you can query the documentation dynamically by asking a question.
Perform an HTTP GET request on a page URL with the \`ask\` query parameter:
\`\`\`
GET ${context.linker.toAbsoluteURL(
context.linker.toPathForPagePath({
path: resolveFirstDocument(context.revision.pages, [])?.page.path ?? 'index',
})
)}?ask=<question>
\`\`\`
The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.
Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
`;
}
+2 -11
View File
@@ -2,7 +2,7 @@ import type { RevisionPageDocument, RevisionPageGroup } from '@gitbook/api';
import { resolveMissingPagePath } from '@/components/SitePage/fetch';
import { isAIEnabled } from '@/components/utils/isAIChatEnabled';
import { renderQueryingDocumentation } from '@/lib/ask-prompt';
import { renderPageAgentInstructions, renderQueryingDocumentation } from '@/lib/ask-prompt';
import type { GitBookSiteContext } from '@/lib/context';
import { getExposableError } from '@/lib/data';
import { linkerWithMarkdownPages } from '@/lib/links';
@@ -184,16 +184,7 @@ function renderAskFooter(
})
);
return `\n\n---\n\n# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.
## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.
${renderQueryingDocumentation({ pageUrl })}
Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
`;
return `\n\n---\n\n${renderPageAgentInstructions({ pageUrl })}`;
}
/**
@@ -152,7 +152,7 @@ export function ElementWebframe(props: ContentKitClientElementProps<ContentKitWe
window.addEventListener('message', callback);
// We only render the iframe once we have added the event listener
// We only load the iframe once we have added the event listener
// otherwise during SSR, we'll miss messages
setMounted(true);
@@ -186,27 +186,27 @@ export function ElementWebframe(props: ContentKitClientElementProps<ContentKitWe
? iframeRef.current.clientWidth / height
: undefined);
if (!mounted) {
return <Icon icon="spinner" className="contentkit-button-loading" style={{ height }} />;
}
// only use height measurement if no aspect ratio is defined
const useHeightMeasurement = !aspectRatio && height;
return (
<iframe
ref={iframeRef}
src={element.source.url}
title={element.source.url}
allowFullScreen
allow="clipboard-write"
className="contentkit-webframe h-full w-full max-w-full border-none"
style={{
aspectRatio,
height: useHeightMeasurement ? height : undefined,
maxHeight: height,
}}
/>
<div className="contentkit-webframe-container h-full w-full max-w-full">
<iframe
ref={iframeRef}
// Rendered without a source before mount so it reserves its size in the SSR layout.
src={mounted ? element.source.url : undefined}
title={element.source.url}
allowFullScreen
allow="clipboard-write"
className="contentkit-webframe block h-full w-full max-w-full border-none"
style={{
aspectRatio,
height: useHeightMeasurement ? height : undefined,
maxHeight: height,
}}
/>
{mounted ? null : <Icon icon="spinner" className="contentkit-webframe-loading" />}
</div>
);
}