mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-10-01 14:38:39 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 67c8ed8abf | |||
| 05945f4b53 | |||
| 7551e7e15a | |||
| d595716d6d | |||
| 96325161be | |||
| d832a54689 | |||
| 356e1f82f2 | |||
| 9f5290bccc | |||
| 61992424af | |||
| 380af10236 | |||
| 0911abc55c | |||
| d8dc59b1a9 | |||
| d04f8bc05e | |||
| 576a781a8f |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Recolor text over a background page cover in the first frame after hydration.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix page cover image jumping on load
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix inline Ask AI inputs and buttons doing nothing before search is opened.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Send the previous page's URL with site insights events so broken links can be traced to the page linking to them.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix Loom videos staying blank on pages with many Loom embeds by lazy-loading them.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Improve dark theme contrast for Mermaid edge labels.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Improve the prompt for agents to ask questions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Point the "Powered by GitBook" trademark link to gitbook.com/powered-by.
|
||||
@@ -129,8 +129,8 @@
|
||||
"@gitbook/react-openapi": "workspace:*",
|
||||
"@mermaid-js/mermaid-zenuml": "^0.2.2",
|
||||
"@modelcontextprotocol/sdk": "1.17.5",
|
||||
"@opennextjs/aws": "4.1.3",
|
||||
"@opennextjs/cloudflare": "1.20.5",
|
||||
"@opennextjs/aws": "4.1.6",
|
||||
"@opennextjs/cloudflare": "1.20.7",
|
||||
"@panzoom/panzoom": "^4.6.1",
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
@@ -346,7 +346,6 @@
|
||||
},
|
||||
"overrides": {
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@opennextjs/aws": "4.1.5",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"axios": "1.8.4",
|
||||
@@ -356,7 +355,7 @@
|
||||
},
|
||||
"catalog": {
|
||||
"@base-ui/react": "^1.7.0",
|
||||
"@gitbook/api": "0.202.0",
|
||||
"@gitbook/api": "0.203.0",
|
||||
"@scalar/api-client-react": "^1.3.46",
|
||||
"@tsconfig/node20": "^20.1.6",
|
||||
"@tsconfig/strictest": "^2.0.6",
|
||||
@@ -728,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.202.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-qhrjEQbNNmCljR0AgP79+BsVh9yelh9TIBg9X3TyYx5a3lJwDiT3sw1ND9DFQSxGHp8hH5w+20KJHn/IDpTbaw=="],
|
||||
"@gitbook/api": ["@gitbook/api@0.203.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-cSFUMM7cIMTHW9cdMpexGSkHHaXmc506fx+LQE2+KLqce8GlBDU/ElIP4RU0MGtN+SSYF31AUgvPD8ZerBVcMA=="],
|
||||
|
||||
"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],
|
||||
|
||||
@@ -944,9 +943,9 @@
|
||||
|
||||
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
||||
|
||||
"@opennextjs/aws": ["@opennextjs/aws@4.1.5", "", { "dependencies": { "@ast-grep/napi": "^0.40.5", "@aws-sdk/client-cloudfront": "3.984.0", "@aws-sdk/client-dynamodb": "3.984.0", "@aws-sdk/client-lambda": "3.984.0", "@aws-sdk/client-s3": "3.984.0", "@aws-sdk/client-sqs": "3.984.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.3", "aws4fetch": "^1.0.20", "chalk": "^5.6.2", "cookie": "^1.0.2", "esbuild": "0.25.4", "express": "^5.1.0", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.1.0", "yaml": "^2.8.1" }, "peerDependencies": { "next": ">=15.5.24 <16 || >=16.3.3" }, "bin": { "open-next": "dist/index.js" } }, "sha512-41LNGtS5R5SCfn9zFxHx9QKJIRnmf1uZH+jonHkUMlRKER0bSmUSGjK6GJF7e50XdLXFpTHzaqgRvGmzvr9Btw=="],
|
||||
"@opennextjs/aws": ["@opennextjs/aws@4.1.6", "", { "dependencies": { "@ast-grep/napi": "^0.40.5", "@aws-sdk/client-cloudfront": "3.984.0", "@aws-sdk/client-dynamodb": "3.984.0", "@aws-sdk/client-lambda": "3.984.0", "@aws-sdk/client-s3": "3.984.0", "@aws-sdk/client-sqs": "3.984.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.3", "aws4fetch": "^1.0.20", "chalk": "^5.6.2", "cookie": "^1.0.2", "esbuild": "0.25.4", "express": "^5.1.0", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.1.0", "yaml": "^2.8.1" }, "peerDependencies": { "next": ">=15.5.26 <16 || >=16.3.6" }, "bin": { "open-next": "dist/index.js" } }, "sha512-J5mzpWo6duetc3ZtcyBxrUqtD6Dwh8msRcMTNQdvpsgTUjEFp03AJmSOnJ/7BKkuF791YTQgjk4iJYIIYLmKqA=="],
|
||||
|
||||
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.20.5", "", { "dependencies": { "@ast-grep/napi": "^0.40.5", "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "4.1.3", "ci-info": "^4.2.0", "cloudflare": "^4.4.1", "comment-json": "^4.5.1", "enquirer": "^2.4.1", "glob": "^12.0.0", "ts-tqdm": "^0.8.6", "yargs": "^18.0.0" }, "peerDependencies": { "next": ">=15.5.24 <16 || >=16.3.3", "rclone.js": "^0.6.6", "wrangler": "^4.125.0" }, "optionalPeers": ["rclone.js"], "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-Y4qCnHTYMa8waEHvf7iqDJuiresuREp0zithSZ8hT33Wn9WyvOUMBM0SWNH961Wv7ynHRdxAJ0ggmnQ3aSdzdg=="],
|
||||
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.20.7", "", { "dependencies": { "@ast-grep/napi": "^0.40.5", "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "4.1.6", "ci-info": "^4.2.0", "cloudflare": "^4.4.1", "comment-json": "^4.5.1", "enquirer": "^2.4.1", "glob": "^12.0.0", "ts-tqdm": "^0.8.6", "yargs": "^18.0.0" }, "peerDependencies": { "next": ">=15.5.26 <16 || >=16.3.6", "rclone.js": "^0.6.6", "wrangler": "^4.125.0" }, "optionalPeers": ["rclone.js"], "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-obZ5l96+MUmGqKd7akJlT9tv0XAvaUDDJj13MyGTVTzA+lbal8kNRoUwei08rtu1Gv+anASAwwMP3dvzcZx+Jw=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
|
||||
+2
-3
@@ -19,8 +19,7 @@
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"esbuild": "0.27.3",
|
||||
"axios": "1.8.4",
|
||||
"@opennextjs/aws": "4.1.5"
|
||||
"axios": "1.8.4"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -49,7 +48,7 @@
|
||||
"@tsconfig/strictest": "^2.0.6",
|
||||
"@tsconfig/node20": "^20.1.6",
|
||||
"@base-ui/react": "^1.7.0",
|
||||
"@gitbook/api": "0.202.0",
|
||||
"@gitbook/api": "0.203.0",
|
||||
"@scalar/api-client-react": "^1.3.46",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
"@gitbook/react-openapi": "workspace:*",
|
||||
"@mermaid-js/mermaid-zenuml": "^0.2.2",
|
||||
"@modelcontextprotocol/sdk": "1.17.5",
|
||||
"@opennextjs/aws": "4.1.3",
|
||||
"@opennextjs/cloudflare": "1.20.5",
|
||||
"@opennextjs/aws": "4.1.6",
|
||||
"@opennextjs/cloudflare": "1.20.7",
|
||||
"@panzoom/panzoom": "^4.6.1",
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
|
||||
@@ -119,9 +119,13 @@ export function useAI(): AIContext {
|
||||
icon: <AISearchIcon />,
|
||||
open: (query?: string) => {
|
||||
if (query) {
|
||||
setSearchState((prev) =>
|
||||
prev ? { ...prev, query: null, ask: query, open: true } : null
|
||||
);
|
||||
setSearchState((prev) => ({
|
||||
...prev,
|
||||
query: null,
|
||||
ask: query,
|
||||
scope: prev?.scope ?? 'default',
|
||||
open: true,
|
||||
}));
|
||||
}
|
||||
},
|
||||
pageAction: false,
|
||||
|
||||
@@ -247,11 +247,15 @@ async function renderMermaidDiagram(args: {
|
||||
const { source, id, darkMode, mermaidRuntimeURL } = args;
|
||||
const { mermaid } = await loadMermaid(mermaidRuntimeURL);
|
||||
|
||||
// Mermaid's default dark edge label pill only reaches 4.43:1 contrast, below WCAG AA.
|
||||
const themeVariables = darkMode ? { edgeLabelBackground: '#3a3a3a' } : undefined;
|
||||
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
securityLevel: 'strict',
|
||||
darkMode,
|
||||
theme: darkMode ? 'dark' : undefined,
|
||||
themeVariables,
|
||||
});
|
||||
|
||||
const renderContainer = createMermaidRenderContainer();
|
||||
|
||||
@@ -37,7 +37,10 @@ export async function Embed(props: BlockProps<gitbookAPI.DocumentBlockEmbed>) {
|
||||
<>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: embed.html,
|
||||
__html:
|
||||
context.mode !== 'print' && shouldLazyLoad(block.data.url)
|
||||
? lazyLoadIframes(embed.html)
|
||||
: embed.html,
|
||||
}}
|
||||
data-visual-test="blackout"
|
||||
/>
|
||||
@@ -73,6 +76,23 @@ export async function Embed(props: BlockProps<gitbookAPI.DocumentBlockEmbed>) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pages with many Loom embeds exhaust the browser's request budget
|
||||
* (ERR_INSUFFICIENT_RESOURCES) when every player loads at once, leaving some blank.
|
||||
*/
|
||||
function lazyLoadIframes(html: string): string {
|
||||
return html.replace(/<iframe\b(?![^>]*\bloading=)/gi, '<iframe loading="lazy"');
|
||||
}
|
||||
|
||||
function shouldLazyLoad(url: string): boolean {
|
||||
try {
|
||||
const { hostname } = new URL(url);
|
||||
return hostname === 'loom.com' || hostname.endsWith('.loom.com');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an integration block with an unfurl action from the GitBook Embed response.
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as React from 'react';
|
||||
import { useDebounceCallback, useEventCallback } from 'usehooks-ts';
|
||||
|
||||
import type * as api from '@gitbook/api';
|
||||
import { SiteInsightsDisplayContext } from '@gitbook/api';
|
||||
import { OpenAPIOperationContextProvider } from '@gitbook/react-openapi';
|
||||
|
||||
import { type CurrentContentContext, useCurrentContent } from '../hooks';
|
||||
@@ -75,12 +76,15 @@ export function InsightsProvider(props: InsightsProviderProps) {
|
||||
[pathname: string]:
|
||||
| {
|
||||
url: string;
|
||||
previousUrl: string | null;
|
||||
events: TrackEventInput<InsightsEventName>[];
|
||||
context: CurrentContentContext;
|
||||
pageContext?: InsightsEventPageContext;
|
||||
}
|
||||
| undefined;
|
||||
}>({});
|
||||
// Href of the last page an event was tracked on; `undefined` until the first event.
|
||||
const lastUrlRef = React.useRef<string | null | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Synchronously flush all the pending events.
|
||||
@@ -105,6 +109,7 @@ export function InsightsProvider(props: InsightsProviderProps) {
|
||||
allEvents.push(
|
||||
...transformEvents({
|
||||
url: eventsForPathname.url,
|
||||
previousUrl: eventsForPathname.previousUrl,
|
||||
events: eventsForPathname.events,
|
||||
context: currentContent,
|
||||
pageContext: eventsForPathname.pageContext,
|
||||
@@ -154,6 +159,9 @@ export function InsightsProvider(props: InsightsProviderProps) {
|
||||
) => {
|
||||
const pathname = window.location.pathname;
|
||||
const previous = eventsRef.current[pathname];
|
||||
const lastUrl =
|
||||
lastUrlRef.current === undefined ? getSameOriginReferrer() : lastUrlRef.current;
|
||||
lastUrlRef.current = window.location.href;
|
||||
eventsRef.current[pathname] = {
|
||||
// An explicitly-provided context wins so page-scoped events (e.g. feedback) can
|
||||
// attribute to their page even when the pathname's ambient context has none — such
|
||||
@@ -161,6 +169,7 @@ export function InsightsProvider(props: InsightsProviderProps) {
|
||||
// context keep the stored one.
|
||||
pageContext: ctx ?? previous?.pageContext,
|
||||
url: previous?.url ?? window.location.href,
|
||||
previousUrl: previous ? previous.previousUrl : lastUrl,
|
||||
events: [
|
||||
...(previous?.events ?? []),
|
||||
{
|
||||
@@ -214,6 +223,18 @@ export function useTrackEvent(): TrackEventCallback {
|
||||
return React.useContext(InsightsContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* The referrer when it's another page of this origin, so a full page load (e.g. an absolute link
|
||||
* to a missing page) still records the page it came from.
|
||||
*/
|
||||
function getSameOriginReferrer(): string | null {
|
||||
if (document.referrer === window.location.href || !URL.canParse(document.referrer)) {
|
||||
return null;
|
||||
}
|
||||
const referrer = new URL(document.referrer);
|
||||
return referrer.origin === window.location.origin ? referrer.href : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post the events to the server.
|
||||
*/
|
||||
@@ -240,6 +261,7 @@ function sendEvents(args: { eventUrl: string; events: api.SiteInsightsEvent[] })
|
||||
*/
|
||||
function transformEvents(input: {
|
||||
url: string;
|
||||
previousUrl: string | null;
|
||||
events: TrackEventInput<InsightsEventName>[];
|
||||
context: CurrentContentContext;
|
||||
pageContext: InsightsEventPageContext;
|
||||
@@ -258,6 +280,11 @@ function transformEvents(input: {
|
||||
|
||||
const location: api.SiteInsightsEventLocation = {
|
||||
url: input.url,
|
||||
// The embed's navigation is between its own tabs, not pages of the site.
|
||||
previousUrl:
|
||||
input.pageContext.displayContext === SiteInsightsDisplayContext.Embed
|
||||
? null
|
||||
: input.previousUrl,
|
||||
siteSection: input.context.siteSectionId ?? null,
|
||||
siteSpace: input.context.siteSpaceId ?? null,
|
||||
space: input.context.spaceId,
|
||||
|
||||
@@ -29,18 +29,21 @@ interface PageCoverImageProps {
|
||||
|
||||
export function PageCoverImage(props: PageCoverImageProps) {
|
||||
const { imgs, y, height, mask } = props;
|
||||
const { containerRef, objectPositionY, isLoading } = useCoverPosition(imgs, y);
|
||||
const { objectPositionY, isLoading } = useCoverPosition(imgs, y, {
|
||||
height,
|
||||
aspectRatio: PAGE_COVER_SIZE,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="h-full w-full overflow-hidden" ref={containerRef}>
|
||||
<div className="h-full w-full overflow-hidden">
|
||||
<div className="h-full w-full animate-pulse bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-900" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full w-full overflow-hidden" ref={containerRef} style={{ height }}>
|
||||
<div className="h-full w-full overflow-hidden @container" style={{ height }}>
|
||||
<img
|
||||
src={imgs.light.src}
|
||||
srcSet={imgs.light.srcSet}
|
||||
@@ -52,7 +55,7 @@ export function PageCoverImage(props: PageCoverImageProps) {
|
||||
aspectRatio: height
|
||||
? undefined
|
||||
: `${PAGE_COVER_SIZE.width}/${PAGE_COVER_SIZE.height}`,
|
||||
objectPosition: `50% ${objectPositionY}%`,
|
||||
objectPosition: `50% ${objectPositionY}`,
|
||||
height, // if no height is passed, no height will be set.
|
||||
maskComposite: 'intersect',
|
||||
maskImage:
|
||||
@@ -73,7 +76,7 @@ export function PageCoverImage(props: PageCoverImageProps) {
|
||||
aspectRatio: height
|
||||
? undefined
|
||||
: `${PAGE_COVER_SIZE.width}/${PAGE_COVER_SIZE.height}`,
|
||||
objectPosition: `50% ${objectPositionY}%`,
|
||||
objectPosition: `50% ${objectPositionY}`,
|
||||
height, // if no height is passed, no height will be set.
|
||||
maskComposite: 'intersect',
|
||||
maskImage:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useResizeObserver } from 'usehooks-ts';
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
|
||||
interface ImageSize {
|
||||
width: number;
|
||||
@@ -22,19 +21,18 @@ interface Images {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to calculate the object position Y percentage for a cover image
|
||||
* based on the y offset, image dimensions, and container dimensions.
|
||||
* Hook to compute the CSS object position Y for a cover image, from the y offset and the image
|
||||
* dimensions. The container must set `container-type: inline-size` and be as wide as the image,
|
||||
* since the position is computed against its width.
|
||||
*/
|
||||
export function useCoverPosition(imgs: Images, y: number) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
export function useCoverPosition(
|
||||
imgs: Images,
|
||||
y: number,
|
||||
container: { height: number | undefined; aspectRatio: ImageSize }
|
||||
) {
|
||||
const [loadedDimensions, setLoadedDimensions] = useState<ImageSize | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(!imgs.light.size && !imgs.dark?.size);
|
||||
|
||||
const container = useResizeObserver({
|
||||
// @ts-expect-error wrong types
|
||||
ref: containerRef,
|
||||
});
|
||||
|
||||
// Load original image dimensions if not provided in `imgs`
|
||||
useLayoutEffect(() => {
|
||||
// Check if we have dimensions from dark (if provided) or else the default light.
|
||||
@@ -68,42 +66,31 @@ export function useCoverPosition(imgs: Images, y: number) {
|
||||
// Check dark first, then light, then loaded dimensions
|
||||
const imageDimensions = imgs.dark?.size ?? imgs.light.size ?? loadedDimensions;
|
||||
|
||||
// Calculate ratio and dimensions similar to useCoverPosition hook
|
||||
const ratio =
|
||||
imageDimensions && container.height && container.width
|
||||
? Math.max(
|
||||
container.width / imageDimensions.width,
|
||||
container.height / imageDimensions.height
|
||||
)
|
||||
: 1;
|
||||
const safeRatio = ratio || 1;
|
||||
|
||||
const scaledHeight =
|
||||
imageDimensions && container.height ? imageDimensions.height * safeRatio : null;
|
||||
const maxOffset =
|
||||
scaledHeight && container.height
|
||||
? Math.max(0, (scaledHeight - container.height) / 2 / safeRatio)
|
||||
: 0;
|
||||
|
||||
// Parse the position between the allowed min/max
|
||||
const objectPositionY = useMemo(() => {
|
||||
if (!container.height || !imageDimensions) {
|
||||
return 50;
|
||||
}
|
||||
|
||||
const scaled = imageDimensions.height * safeRatio;
|
||||
if (scaled <= container.height || maxOffset === 0) {
|
||||
return 50;
|
||||
}
|
||||
|
||||
const clampedOffset = Math.max(-maxOffset, Math.min(maxOffset, y));
|
||||
const relative = (maxOffset - clampedOffset) / (2 * maxOffset);
|
||||
return relative * 100;
|
||||
}, [container.height, imageDimensions, maxOffset, safeRatio, y]);
|
||||
|
||||
return {
|
||||
containerRef,
|
||||
objectPositionY,
|
||||
objectPositionY: imageDimensions
|
||||
? getCoverObjectPositionY(imageDimensions, y, container)
|
||||
: '50%',
|
||||
isLoading: !imageDimensions || isLoading,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset the image `y` natural pixels from centered, clamped so it keeps covering the container.
|
||||
* Expressed in CSS against the container width (`cqw`), so it renders the same on the server as
|
||||
* after hydration, without measuring the container.
|
||||
*/
|
||||
function getCoverObjectPositionY(
|
||||
image: ImageSize,
|
||||
y: number,
|
||||
container: { height: number | undefined; aspectRatio: ImageSize }
|
||||
): string {
|
||||
const containerHeight = container.height
|
||||
? `${container.height}px`
|
||||
: `${(100 * container.aspectRatio.height) / container.aspectRatio.width}cqw`;
|
||||
// Rendered height of the image under `object-fit: cover`.
|
||||
const scaledHeight = `max(${(100 * image.height) / image.width}cqw, ${containerHeight})`;
|
||||
const maxOffset = `(${scaledHeight} - ${containerHeight}) / 2`;
|
||||
const offset = `${scaledHeight} * ${y / image.height}`;
|
||||
|
||||
return `calc(50% + clamp(-1 * ${maxOffset}, ${offset}, ${maxOffset}))`;
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ type CoverOverlap =
|
||||
* `data-over-cover="split"` with the crossing point in `--cover-edge`.
|
||||
*/
|
||||
function useMarkTextOverCover() {
|
||||
React.useEffect(() => {
|
||||
React.useLayoutEffect(() => {
|
||||
const root = document.documentElement;
|
||||
const pageCover = document.querySelector<HTMLElement>('[data-gb-page-cover]');
|
||||
|
||||
@@ -150,7 +150,7 @@ function useMarkTextOverCover() {
|
||||
});
|
||||
};
|
||||
|
||||
scheduleUpdate();
|
||||
update();
|
||||
|
||||
window.addEventListener('scroll', scheduleUpdate, { passive: true });
|
||||
window.addEventListener('resize', scheduleUpdate, { passive: true });
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function Trademark(
|
||||
const { space } = context;
|
||||
const language = await getSpaceLanguage(context);
|
||||
|
||||
const url = new URL('https://www.gitbook.com');
|
||||
const url = new URL('https://www.gitbook.com/powered-by');
|
||||
url.searchParams.set('utm_source', 'content');
|
||||
url.searchParams.set('utm_medium', 'trademark');
|
||||
url.searchParams.set('utm_campaign', space.id);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the "Querying This Documentation" section of the agent instructions.
|
||||
* `pageUrl` is the URL of the current page, which the `ask` and `goal` parameters are appended to.
|
||||
*/
|
||||
export function renderQueryingDocumentation(options: { pageUrl: string }): string {
|
||||
const { pageUrl } = options;
|
||||
return `Perform an HTTP GET request on the following URL with the \`ask\` and \`goal\` query parameters:
|
||||
|
||||
\`\`\`
|
||||
GET ${pageUrl}?ask=<question>&goal=<user_goal>
|
||||
\`\`\`
|
||||
|
||||
${renderAskParametersDescription()}
|
||||
|
||||
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.`;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { RevisionPageDocument, RevisionPageGroup } from '@gitbook/api';
|
||||
|
||||
import { isAIEnabled } from '@/components/utils/isAIChatEnabled';
|
||||
import { renderQueryingDocumentation } from '@/lib/ask-prompt';
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { getExposableError } from '@/lib/data';
|
||||
import { linkerWithMarkdownPages } from '@/lib/links';
|
||||
@@ -69,6 +70,11 @@ function renderNotFoundMarkdown(context: GitBookSiteContext, pagePath: string) {
|
||||
const fullContentUrl = context.linker.toAbsoluteURL(
|
||||
context.linker.toPathInSite('llms-full.txt')
|
||||
);
|
||||
const askPageUrl = context.linker.toAbsoluteURL(
|
||||
context.linker.toPathForPagePath({
|
||||
path: similarPages[0]?.path ?? 'docs/example',
|
||||
})
|
||||
);
|
||||
|
||||
return `# Page Not Found
|
||||
|
||||
@@ -85,20 +91,7 @@ If the exact page cannot be found, you can still retrieve the information using
|
||||
|
||||
### Option 1 — Ask a question (recommended)
|
||||
|
||||
Perform an HTTP GET request on the documentation index with the \`ask\` parameter, and the optional \`goal\` parameter:
|
||||
|
||||
\`\`\`
|
||||
GET ${context.linker.toAbsoluteURL(
|
||||
context.linker.toPathForPagePath({
|
||||
path: similarPages[0]?.path ?? 'docs/example',
|
||||
})
|
||||
)}?ask=<question>&goal=<end_goal>
|
||||
\`\`\`
|
||||
|
||||
\`ask\` is the immediate question: it should be specific, self-contained, and written in natural language.
|
||||
\`goal\` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.
|
||||
|
||||
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.
|
||||
${renderQueryingDocumentation({ pageUrl: askPageUrl })}
|
||||
|
||||
### Option 2 — Browse the documentation index
|
||||
|
||||
@@ -145,16 +138,7 @@ This documentation is published with GitBook. GitBook is the documentation platf
|
||||
## 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.
|
||||
|
||||
Perform an HTTP GET request on the current page URL with the \`ask\` query parameter, and the optional \`goal\` query parameter:
|
||||
|
||||
\`\`\`
|
||||
GET ${pageUrl}?ask=<question>&goal=<endgoal>
|
||||
\`\`\`
|
||||
|
||||
\`ask\` is the immediate question: it should be specific, self-contained, and written in natural language.
|
||||
\`goal\` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.
|
||||
|
||||
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.
|
||||
${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.
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user