mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 229f2ba9d5 | |||
| 6da36558f6 | |||
| ca71c1fad6 | |||
| 262afa357b | |||
| 7375d3c597 | |||
| 3548fa6dff | |||
| cb73040e0f | |||
| 872d36b64f | |||
| e1ff17e655 | |||
| f3010bd28a | |||
| 8ff1e3b619 | |||
| d7596bf454 | |||
| aea5eb10ae | |||
| 1165a81cf5 | |||
| 61d1a0192e | |||
| f9a2977621 | |||
| 24f601d594 |
@@ -182,9 +182,9 @@ jobs:
|
||||
SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
|
||||
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
|
||||
ARGOS_BUILD_NAME: 'customers-v2'
|
||||
pagespeed-testing-v2:
|
||||
browserless-testing-v2-vercel:
|
||||
runs-on: ubuntu-latest
|
||||
name: PageSpeed Testing v1
|
||||
name: Browserless Testing v2 (Vercel)
|
||||
needs: deploy-v2-vercel
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -195,8 +195,26 @@ jobs:
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- name: Run pagespeed tests
|
||||
run: bun ./packages/gitbook/tests/pagespeed-testing.ts
|
||||
- name: Run tests
|
||||
run: cd ./packages/gitbook && bun e2e-browserless
|
||||
env:
|
||||
BASE_URL: ${{needs.deploy-v2-vercel.outputs.deployment-url}}
|
||||
PAGESPEED_API_KEY: ${{ secrets.PAGESPEED_API_KEY }}
|
||||
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
|
||||
# browserless-testing-v2-cloudflare:
|
||||
# runs-on: ubuntu-latest
|
||||
# name: Browserless Testing v2 (Cloudflare)
|
||||
# needs: deploy-v2-cloudflare
|
||||
# steps:
|
||||
# - name: Checkout
|
||||
# uses: actions/checkout@v4
|
||||
# - name: Setup Bun
|
||||
# uses: ./.github/composite/setup-bun
|
||||
# - name: Install dependencies
|
||||
# run: bun install --frozen-lockfile
|
||||
# env:
|
||||
# PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
# - name: Run tests
|
||||
# run: cd ./packages/gitbook && bun e2e-browserless
|
||||
# env:
|
||||
# BASE_URL: ${{needs.deploy-v2-cloudflare.outputs.deployment-url}}
|
||||
# SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
|
||||
+1
-1
@@ -34,7 +34,7 @@
|
||||
"workspaces": {
|
||||
"packages": ["packages/*"],
|
||||
"catalog": {
|
||||
"@gitbook/api": "^0.139.0",
|
||||
"@gitbook/api": "^0.141.0",
|
||||
"bidc": "^0.0.2"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# @gitbook/expr
|
||||
|
||||
## 1.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 6da3655: Fix exports in gitbook/expr package.json
|
||||
|
||||
## 1.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3548fa6: Fix eval-estree-expr named import
|
||||
|
||||
## 1.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- e1ff17e: Fix bundling of gitbook/expr package
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8ff1e3b: Add support for every/some array methods
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
@@ -1,34 +1,37 @@
|
||||
{
|
||||
"name": "@gitbook/expr",
|
||||
"description": "Safely evaluate & parse user-defined GitBook expressions.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"development": "./src/index.ts",
|
||||
"import": "./dist/index.js",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"eval-estree-expression": "^2.0.3",
|
||||
"eval-estree-expression": "github:jonschlinkert/eval-estree-expression#9cf28d2",
|
||||
"acorn": "^8.14.0",
|
||||
"acorn-loose": "8.4.0",
|
||||
"acorn-walk": "^8.3.4",
|
||||
"escodegen": "^2.1.0",
|
||||
"assert-never": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"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"
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@types/escodegen": "^0.0.10"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"build": "tsdown --project tsconfig.build.json",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"unit": "bun test",
|
||||
"clean": "rm -rf ./dist"
|
||||
},
|
||||
"files": ["dist", "src", "README.md", "CHANGELOG.md"]
|
||||
"files": ["dist", "README.md", "CHANGELOG.md"]
|
||||
}
|
||||
|
||||
@@ -210,6 +210,8 @@ describe('autocomplete', () => {
|
||||
'visitor.claims.hello.length',
|
||||
'visitor.claims.hello.at',
|
||||
'visitor.claims.hello.includes',
|
||||
'visitor.claims.hello.some',
|
||||
'visitor.claims.hello.every',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -268,6 +270,8 @@ describe('autocomplete', () => {
|
||||
'visitor.claims.hello.length',
|
||||
'visitor.claims.hello.at',
|
||||
'visitor.claims.hello.includes',
|
||||
'visitor.claims.hello.some',
|
||||
'visitor.claims.hello.every',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -42,11 +42,29 @@ describe('ExpressionRuntime', () => {
|
||||
},
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'array method',
|
||||
condition: 'reviews.every(review => !!review.status)',
|
||||
inputs: { reviews: [{ status: 'approved' }, { status: 'approved' }] },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'array every',
|
||||
condition: 'reviews.every(review => review.status === "approved")',
|
||||
inputs: { reviews: [{ status: 'approved' }, { status: 'approved' }] },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
scenario: 'array map',
|
||||
condition: '[1, 2, 3].map(n => n * x)',
|
||||
inputs: { x: 2 },
|
||||
expectedResult: [2, 4, 6],
|
||||
},
|
||||
])(
|
||||
'should properly evaluate/safeEvaluate a valid conditional expression: $scenario',
|
||||
({ condition, inputs, expectedResult }) => {
|
||||
expect(runtime.evaluate(condition, inputs)).toBe(expectedResult);
|
||||
expect(runtime.safeEvaluate(condition, inputs).value).toBe(expectedResult);
|
||||
expect(runtime.evaluate(condition, inputs)).toEqual(expectedResult);
|
||||
expect(runtime.safeEvaluate(condition, inputs).value).toEqual(expectedResult);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export * from './errors';
|
||||
export * from './input-values';
|
||||
export * from './input-values';
|
||||
export * from './runtime';
|
||||
export * from './symbols';
|
||||
export * from './template';
|
||||
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
tokenizer,
|
||||
} from 'acorn';
|
||||
import { parse as parseLoose } from 'acorn-loose';
|
||||
import { evaluate } from 'eval-estree-expression';
|
||||
import escodegen from 'escodegen';
|
||||
import evalESTreeExpr from 'eval-estree-expression';
|
||||
const { evaluate } = evalESTreeExpr;
|
||||
|
||||
import { AutoComplete } from './autocomplete';
|
||||
import { ExpressionError } from './errors';
|
||||
@@ -49,6 +51,7 @@ export class ExpressionRuntime {
|
||||
return evaluate.sync<Expression>(parsed.result, inputs, {
|
||||
functions: true,
|
||||
withMembers: true,
|
||||
generate: escodegen.generate,
|
||||
});
|
||||
} catch (error) {
|
||||
throw error instanceof Error
|
||||
|
||||
@@ -338,6 +338,8 @@ describe('ExpressionRuntime', () => {
|
||||
'visitor.claims.hello.length',
|
||||
'visitor.claims.hello.at',
|
||||
'visitor.claims.hello.includes',
|
||||
'visitor.claims.hello.some',
|
||||
'visitor.claims.hello.every',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -259,6 +259,60 @@ const StandardLibrary: Partial<
|
||||
'true if the value searchElement is found within the array (or the part of the array indicated by the index fromIndex, if specified).',
|
||||
}),
|
||||
}),
|
||||
SymbolFunction({
|
||||
name: 'some',
|
||||
description:
|
||||
'Tests whether at least one element in the array passes the test implemented by the provided function.',
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some',
|
||||
args: [
|
||||
SymbolFunction({
|
||||
name: 'callback',
|
||||
description: 'A function that tests each element of the array.',
|
||||
args: [
|
||||
{
|
||||
...arraySymbolDef.items,
|
||||
name: 'element',
|
||||
description: 'The current element being processed in the array.',
|
||||
},
|
||||
],
|
||||
returns: SymbolBoolean({
|
||||
description:
|
||||
'true if the callback function returns a truthy value for at least one element in the array.',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
returns: SymbolBoolean({
|
||||
description:
|
||||
'true if the callback function returns a truthy value for at least one element in the array.',
|
||||
}),
|
||||
}),
|
||||
SymbolFunction({
|
||||
name: 'every',
|
||||
description:
|
||||
'Tests whether all elements in the array pass the test implemented by the provided function.',
|
||||
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every',
|
||||
args: [
|
||||
SymbolFunction({
|
||||
name: 'callback',
|
||||
description: 'A function that tests each element of the array.',
|
||||
args: [
|
||||
{
|
||||
...arraySymbolDef.items,
|
||||
name: 'element',
|
||||
description: 'The current element being processed in the array.',
|
||||
},
|
||||
],
|
||||
returns: SymbolBoolean({
|
||||
description:
|
||||
'true if the callback function returns a truthy value for all elements in the array.',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
returns: SymbolBoolean({
|
||||
description:
|
||||
'true if the callback function returns a truthy value for all elements in the array.',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'tsdown';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
entry: ['src/index.ts'],
|
||||
dts: true,
|
||||
format: ['esm'],
|
||||
},
|
||||
]);
|
||||
+1
-1
@@ -14,7 +14,7 @@ declare module 'eval-estree-expression' {
|
||||
/**
|
||||
* Enable support for function statements and expressions by enabling the functions option AND by passing the .generate() function from the escodegen library. Default: undefined
|
||||
*/
|
||||
generate?: boolean;
|
||||
generate?: boolean | ((node: any) => string);
|
||||
/**
|
||||
* Enable the =~ regex operator to support testing values without using functions (example name =~ /^a.*c$/). Default: true
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
# gitbook
|
||||
|
||||
## 0.18.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 262afa3: Expose a MCP server for the docs site under /~gitbook/mcp
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [262afa3]
|
||||
- @gitbook/icons@0.3.1
|
||||
- @gitbook/react-openapi@1.4.3
|
||||
- @gitbook/react-contentkit@0.7.5
|
||||
|
||||
## 0.17.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @gitbook/react-openapi@1.4.2
|
||||
|
||||
## 0.17.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 24f601d: Small optim in resolveTryItPrefillForOperation
|
||||
- aea5eb1: Persist language choice across sections if possible
|
||||
- 1165a81: Language selector edge cases
|
||||
- f9a2977: Better handling for external link "mailto:" in Hovered Card in GBO
|
||||
- Updated dependencies [24f601d]
|
||||
- @gitbook/react-openapi@1.4.1
|
||||
|
||||
## 0.17.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -336,6 +336,7 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
|
||||
pageActions: {
|
||||
externalAI: true,
|
||||
markdown: true,
|
||||
mcp: true,
|
||||
},
|
||||
trademark: {
|
||||
enabled: true,
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"dev": {
|
||||
"vars": {
|
||||
"STAGE": "dev",
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true"
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true",
|
||||
"GITBOOK_URL": "http://localhost:8771"
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
{
|
||||
"name": "gitbook",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/browser-types": "workspace:*",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@gitbook/colors": "workspace:*",
|
||||
"@gitbook/expr": "workspace:*",
|
||||
"@gitbook/emoji-codepoints": "workspace:*",
|
||||
"@gitbook/fonts": "workspace:*",
|
||||
"@gitbook/icons": "workspace:*",
|
||||
@@ -47,7 +46,7 @@
|
||||
"memoizee": "^0.4.17",
|
||||
"micromark-extension-frontmatter": "^2.0.0",
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"next": "15.3.5",
|
||||
"next": "15.4.0",
|
||||
"next-themes": "^0.2.1",
|
||||
"nuqs": "^2.2.3",
|
||||
"object-hash": "^3.0.0",
|
||||
@@ -72,7 +71,10 @@
|
||||
"url-join": "^5.0.0",
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"warn-once": "^0.1.1",
|
||||
"zustand": "^5.0.3"
|
||||
"zustand": "^5.0.3",
|
||||
"mcp-handler": "^1.0.2",
|
||||
"@modelcontextprotocol/sdk": "^1.17.5",
|
||||
"zod": "^3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@argos-ci/playwright": "^5.0.9",
|
||||
@@ -85,7 +87,6 @@
|
||||
"@types/node": "^20",
|
||||
"@types/object-hash": "^3.0.6",
|
||||
"@types/parse-cache-control": "^1.0.4",
|
||||
"@types/psi": "^4.1.6",
|
||||
"@types/react": "18.3.13",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@types/rison": "^0.0.9",
|
||||
@@ -93,7 +94,6 @@
|
||||
"env-cmd": "^10.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"postcss": "^8",
|
||||
"psi": "^4.1.0",
|
||||
"stylelint": "^16.16.0",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"ts-essentials": "^10.0.1",
|
||||
@@ -114,6 +114,7 @@
|
||||
"e2e": "playwright test e2e/internal.spec.ts e2e/pdf.spec.ts --project=chromium",
|
||||
"e2e-customers": "playwright test e2e/customers.spec.ts --project=chromium",
|
||||
"unit": "bun test {src,packages} --preload ./tests/preload-bun.ts",
|
||||
"e2e-browserless": "bun test ./tests/",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"browserslist": [
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import { type RouteLayoutParams, getStaticSiteContext } from '@/app/utils';
|
||||
import { throwIfDataError } from '@/lib/data';
|
||||
import { joinPathWithBaseURL } from '@/lib/paths';
|
||||
import { findSiteSpaceBy } from '@/lib/sites';
|
||||
import { createMcpHandler } from 'mcp-handler';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
async function handler(
|
||||
nextRequest: NextRequest,
|
||||
{ params }: { params: Promise<RouteLayoutParams> }
|
||||
) {
|
||||
const { context } = await getStaticSiteContext(await params);
|
||||
const { dataFetcher, linker, site } = context;
|
||||
|
||||
const mcpHandler = createMcpHandler(
|
||||
(server) => {
|
||||
server.tool(
|
||||
'searchDocumentation',
|
||||
`Search across the documentation to find relevant information, code examples, API references, and guides. Use this tool when you need to answer questions about ${site.title}, find specific documentation, understand how features work, or locate implementation details. The search returns contextual content with titles and direct links to the documentation pages.`,
|
||||
{
|
||||
query: z.string(),
|
||||
},
|
||||
async ({ query }) => {
|
||||
const results = await throwIfDataError(
|
||||
dataFetcher.searchSiteContent({
|
||||
organizationId: context.organizationId,
|
||||
siteId: site.id,
|
||||
query,
|
||||
scope: { mode: 'all' },
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
content: results.flatMap((spaceResult) => {
|
||||
const found = findSiteSpaceBy(
|
||||
context.structure,
|
||||
(siteSpace) => siteSpace.space.id === spaceResult.id
|
||||
);
|
||||
const spaceURL = found?.siteSpace.urls.published;
|
||||
if (!spaceURL) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return spaceResult.pages.map((pageResult) => {
|
||||
const pageURL = linker.toAbsoluteURL(
|
||||
linker.toLinkForContent(
|
||||
joinPathWithBaseURL(spaceURL, pageResult.path)
|
||||
)
|
||||
);
|
||||
|
||||
const body = pageResult.sections
|
||||
?.map((section) => section.body)
|
||||
.join('\n');
|
||||
|
||||
return {
|
||||
type: 'text',
|
||||
text: [
|
||||
`Title: ${pageResult.title}`,
|
||||
`Link: ${pageURL}`,
|
||||
body ? `Content: ${body}` : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n'),
|
||||
};
|
||||
});
|
||||
}),
|
||||
};
|
||||
}
|
||||
);
|
||||
},
|
||||
{},
|
||||
{
|
||||
basePath: context.linker.toPathInSite('~gitbook/'),
|
||||
streamableHttpEndpoint: '/mcp',
|
||||
maxDuration: 60,
|
||||
verboseLogs: true,
|
||||
disableSse: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Next.js request.url is the original URL and not the rewritten one from the middleware
|
||||
const requestURL = new URL(
|
||||
context.linker.toAbsoluteURL(context.linker.toPathInSite('~gitbook/mcp'))
|
||||
);
|
||||
requestURL.search = nextRequest.nextUrl.search;
|
||||
|
||||
const request = new Request(requestURL, nextRequest);
|
||||
return mcpHandler(request);
|
||||
}
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { headers } from 'next/headers';
|
||||
import React from 'react';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { DateRelative } from '../primitives';
|
||||
import { IframeWrapper } from './IframeWrapper';
|
||||
import { RefreshChangeRequestButton } from './RefreshChangeRequestButton';
|
||||
import { Toolbar, ToolbarBody, ToolbarButton, ToolbarButtonGroups } from './Toolbar';
|
||||
|
||||
@@ -47,19 +46,21 @@ function ToolbarLayout(props: { children: React.ReactNode }) {
|
||||
*/
|
||||
export async function AdminToolbar(props: AdminToolbarProps) {
|
||||
const { context } = props;
|
||||
const mode = (await headers()).get('x-gitbook-mode');
|
||||
|
||||
if (mode === 'multi-id') {
|
||||
// We don't show the admin toolbar in multi-id mode, as it's used for previewing in the dashboard.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (context.changeRequest) {
|
||||
return <ChangeRequestToolbar context={context} />;
|
||||
return (
|
||||
<IframeWrapper>
|
||||
<ChangeRequestToolbar context={context} />
|
||||
</IframeWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (context.revisionId !== context.space.revision) {
|
||||
return <RevisionToolbar context={context} />;
|
||||
return (
|
||||
<IframeWrapper>
|
||||
<RevisionToolbar context={context} />
|
||||
</IframeWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface IframeWrapperProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client component that detects if we're in an iframe and conditionally renders children
|
||||
*/
|
||||
export function IframeWrapper({ children }: IframeWrapperProps) {
|
||||
const [isInIframe, setIsInIframe] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Check if we're running inside an iframe
|
||||
const inIframe = window !== window.parent;
|
||||
setIsInIframe(inIframe);
|
||||
}, []);
|
||||
|
||||
// Don't render children if we're in an iframe (GitBook app preview)
|
||||
if (isInIframe) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from './AdminToolbar';
|
||||
export * from './IframeWrapper';
|
||||
|
||||
@@ -46,6 +46,7 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
|
||||
);
|
||||
}
|
||||
const isExternal = inline.data.ref.kind === 'url';
|
||||
const isMailto = resolved.href.startsWith('mailto:');
|
||||
const content = (
|
||||
<StyledLink
|
||||
href={resolved.href}
|
||||
@@ -63,7 +64,12 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
|
||||
nodes={inline.nodes}
|
||||
ancestorInlines={[...ancestorInlines, inline]}
|
||||
/>
|
||||
{isExternal ? (
|
||||
{isMailto ? (
|
||||
<Icon
|
||||
icon="envelope"
|
||||
className="ml-1 inline size-3 links-accent:text-tint-subtle"
|
||||
/>
|
||||
) : isExternal ? (
|
||||
<Icon
|
||||
icon="arrow-up-right"
|
||||
className="ml-0.5 inline size-3 links-accent:text-tint-subtle"
|
||||
@@ -96,16 +102,24 @@ function InlineLinkTooltipWrapper(props: {
|
||||
const { inline, language, resolved, children } = props;
|
||||
|
||||
let breadcrumbs = resolved.ancestors ?? [];
|
||||
const isMailto = resolved.href.startsWith('mailto:');
|
||||
const isExternal = inline.data.ref.kind === 'url';
|
||||
const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined;
|
||||
if (isExternal) {
|
||||
|
||||
if (isMailto) {
|
||||
resolved.text = resolved.text.split('mailto:')[1] ?? resolved.text;
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: tString(language, 'link_tooltip_email'),
|
||||
},
|
||||
];
|
||||
} else if (isExternal) {
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: tString(language, 'link_tooltip_external_link'),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (isSamePage) {
|
||||
} else if (isSamePage) {
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: tString(language, 'link_tooltip_page_anchor'),
|
||||
|
||||
@@ -15,12 +15,17 @@ import { getSimplifiedContentType } from '@/lib/files';
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { filterOutNullable } from '@/lib/typescript';
|
||||
|
||||
import type { BlockProps } from '../Block';
|
||||
import { Blocks } from '../Blocks';
|
||||
import { FileIcon } from '../FileIcon';
|
||||
import type { TableRecordKV } from './Table';
|
||||
import { type VerticalAlignment, getColumnAlignment } from './utils';
|
||||
import {
|
||||
type VerticalAlignment,
|
||||
getColumnAlignment,
|
||||
isContentRef,
|
||||
isDocumentTableImageRecord,
|
||||
isStringArray,
|
||||
} from './utils';
|
||||
|
||||
const alignmentMap: Record<'text-left' | 'text-center' | 'text-right', string> = {
|
||||
'text-left': '**:text-left text-left',
|
||||
@@ -58,18 +63,28 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
return null;
|
||||
}
|
||||
|
||||
// Because definition and value depends on column, we have to check typing in each case at runtime.
|
||||
// Validation should have been done at the API level, but we can't know typing based on `definition.type`.
|
||||
// OpenAPI types cannot really handle discriminated unions based on a dynamic key.
|
||||
switch (definition.type) {
|
||||
case 'checkbox':
|
||||
case 'checkbox': {
|
||||
if (value === null || typeof value !== 'boolean') {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Checkbox
|
||||
className={tcls('w-5', 'h-5')}
|
||||
checked={value as boolean}
|
||||
checked={value}
|
||||
disabled={true}
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'rating': {
|
||||
const rating = value as number;
|
||||
if (typeof value !== 'number') {
|
||||
return null;
|
||||
}
|
||||
const rating = value;
|
||||
const max = definition.max;
|
||||
|
||||
return (
|
||||
@@ -108,15 +123,21 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
case 'number':
|
||||
case 'number': {
|
||||
if (typeof value !== 'number') {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Tag
|
||||
className={tcls('text-base', 'tabular-nums', 'tracking-tighter')}
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
>{`${value}`}</Tag>
|
||||
);
|
||||
}
|
||||
case 'text': {
|
||||
// @ts-ignore
|
||||
if (typeof value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
const fragment = getNodeFragmentByName(block, value);
|
||||
if (!fragment) {
|
||||
return <Tag className={tcls(['w-full', verticalAlignment])}>{''}</Tag>;
|
||||
@@ -149,8 +170,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
);
|
||||
}
|
||||
case 'files': {
|
||||
if (!isStringArray(value)) {
|
||||
return null;
|
||||
}
|
||||
const files = await Promise.all(
|
||||
(value as string[]).map((fileId) =>
|
||||
value.map((fileId) =>
|
||||
context.contentContext
|
||||
? resolveContentRef(
|
||||
{
|
||||
@@ -221,10 +245,12 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
);
|
||||
}
|
||||
case 'content-ref': {
|
||||
const contentRef = value ? (value as ContentRef) : null;
|
||||
if (value === null || !isContentRef(value)) {
|
||||
return null;
|
||||
}
|
||||
const resolved =
|
||||
contentRef && context.contentContext
|
||||
? await resolveContentRef(contentRef, context.contentContext, {
|
||||
value && context.contentContext
|
||||
? await resolveContentRef(value, context.contentContext, {
|
||||
resolveAnchorText: true,
|
||||
iconStyle: ['mr-2', 'text-tint-subtle'],
|
||||
})
|
||||
@@ -239,11 +265,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
<StyledLink
|
||||
href={resolved.href}
|
||||
insights={
|
||||
contentRef
|
||||
value
|
||||
? {
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: contentRef,
|
||||
target: value,
|
||||
position: SiteInsightsLinkPosition.Content,
|
||||
},
|
||||
}
|
||||
@@ -257,8 +283,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
);
|
||||
}
|
||||
case 'users': {
|
||||
if (!isStringArray(value)) {
|
||||
return null;
|
||||
}
|
||||
const resolved = await Promise.all(
|
||||
(value as string[]).map(async (userId) => {
|
||||
value.map(async (userId) => {
|
||||
const contentRef: ContentRefUser = {
|
||||
kind: 'user',
|
||||
user: userId,
|
||||
@@ -295,10 +324,13 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
);
|
||||
}
|
||||
case 'select': {
|
||||
if (!isStringArray(value)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Tag aria-labelledby={ariaLabelledBy}>
|
||||
<span className={tcls('inline-flex', 'gap-2', 'flex-wrap')}>
|
||||
{(value as string[]).map((selectId) => {
|
||||
{value.map((selectId) => {
|
||||
const option = definition.options.find(
|
||||
(option) => option.value === selectId
|
||||
);
|
||||
@@ -329,8 +361,15 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
);
|
||||
}
|
||||
case 'image': {
|
||||
if (!isDocumentTableImageRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const image = context.contentContext
|
||||
? await resolveContentRef(value as ContentRef, context.contentContext)
|
||||
? await resolveContentRef(
|
||||
'ref' in value ? value.ref : value,
|
||||
context.contentContext
|
||||
)
|
||||
: null;
|
||||
|
||||
if (!image) {
|
||||
|
||||
@@ -123,3 +123,35 @@ export function getColumnVerticalAlignment(column: DocumentTableDefinition): Ver
|
||||
|
||||
return 'self-center';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is a ContentRef.
|
||||
* @param ref The value to check.
|
||||
* @returns True if the value is a ContentRef, false otherwise.
|
||||
*/
|
||||
export function isContentRef(ref?: DocumentTableRecord['values'][string]): ref is ContentRef {
|
||||
return Boolean(ref && typeof ref === 'object' && 'kind' in ref);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is an array of strings.
|
||||
* @param value The value to check.
|
||||
* @returns True if the value is an array of strings, false otherwise.
|
||||
*/
|
||||
export function isStringArray(value?: DocumentTableRecord['values'][string]): value is string[] {
|
||||
return Array.isArray(value) && value.every((v) => typeof v === 'string');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is a DocumentTableImageRecord.
|
||||
* @param value The value to check.
|
||||
* @returns True if the value is a DocumentTableImageRecord, false otherwise.
|
||||
*/
|
||||
export function isDocumentTableImageRecord(
|
||||
value?: DocumentTableRecord['values'][string]
|
||||
): value is DocumentTableImageRecord {
|
||||
if (isContentRef(value) && (value.kind === 'file' || value.kind === 'url')) {
|
||||
return true;
|
||||
}
|
||||
return Boolean(value && typeof value === 'object' && 'ref' in value && isContentRef(value.ref));
|
||||
}
|
||||
|
||||
@@ -80,13 +80,14 @@ export function Header(props: {
|
||||
>
|
||||
<HeaderMobileMenu
|
||||
className={tcls(
|
||||
'lg:hidden',
|
||||
'-ml-2',
|
||||
'text-tint-strong',
|
||||
'theme-bold:text-header-link',
|
||||
'hover:bg-tint-hover',
|
||||
'hover:theme-bold:bg-header-link/3',
|
||||
withVariants === 'generic' ? '' : 'page-no-toc:hidden'
|
||||
withVariants === 'generic'
|
||||
? 'xl:hidden'
|
||||
: 'page-no-toc:hidden lg:hidden'
|
||||
)}
|
||||
/>
|
||||
<HeaderLogo context={context} />
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
|
||||
import { useAIChatState } from '@/components/AI';
|
||||
import type { Assistant } from '@/components/AI';
|
||||
import { ChatGPTIcon } from '@/components/PageActions/assets/ChatGPTIcon';
|
||||
import { ClaudeIcon } from '@/components/PageActions/assets/ClaudeIcon';
|
||||
import { MarkdownIcon } from '@/components/PageActions/assets/MarkdownIcon';
|
||||
import { Button } from '@/components/primitives/Button';
|
||||
import { DropdownMenuItem, useDropdownMenuClose } from '@/components/primitives/DropdownMenu';
|
||||
import { tString, useLanguage } from '@/intl/client';
|
||||
@@ -13,15 +10,15 @@ import type { GitSyncState } from '@gitbook/api';
|
||||
import { Icon, type IconName, IconStyle } from '@gitbook/icons';
|
||||
import assertNever from 'assert-never';
|
||||
import QuickLRU from 'quick-lru';
|
||||
import type React from 'react';
|
||||
import React from 'react';
|
||||
import { create } from 'zustand';
|
||||
|
||||
type PageActionType = 'button' | 'dropdown-menu-item';
|
||||
|
||||
/**
|
||||
* Opens our AI Docs Assistant.
|
||||
* Action to open the GitBook Assistant.
|
||||
*/
|
||||
export function OpenAIAssistant(props: { assistant: Assistant; type: PageActionType }) {
|
||||
export function ActionOpenAssistant(props: { assistant: Assistant; type: PageActionType }) {
|
||||
const { assistant, type } = props;
|
||||
const chat = useAIChatState();
|
||||
const language = useLanguage();
|
||||
@@ -87,14 +84,14 @@ const useCopiedStore = create<
|
||||
const markdownCache = new QuickLRU<string, string>({ maxSize: 10 });
|
||||
|
||||
/**
|
||||
* Copies the markdown version of the page to the clipboard.
|
||||
* Action to copy the markdown version of the page to the clipboard.
|
||||
*/
|
||||
export function CopyMarkdown(props: {
|
||||
markdownPageUrl: string;
|
||||
export function ActionCopyMarkdown(props: {
|
||||
markdownPageURL: string;
|
||||
type: PageActionType;
|
||||
isDefaultAction?: boolean;
|
||||
}) {
|
||||
const { markdownPageUrl, type, isDefaultAction } = props;
|
||||
const { markdownPageURL, type, isDefaultAction } = props;
|
||||
const language = useLanguage();
|
||||
|
||||
const closeDropdown = useDropdownMenuClose();
|
||||
@@ -105,8 +102,8 @@ export function CopyMarkdown(props: {
|
||||
const fetchMarkdown = async () => {
|
||||
setLoading(true);
|
||||
|
||||
const result = await fetch(markdownPageUrl).then((res) => res.text());
|
||||
markdownCache.set(markdownPageUrl, result);
|
||||
const result = await fetch(markdownPageURL).then((res) => res.text());
|
||||
markdownCache.set(markdownPageURL, result);
|
||||
|
||||
setLoading(false);
|
||||
|
||||
@@ -120,7 +117,7 @@ export function CopyMarkdown(props: {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
copy(markdownCache.get(markdownPageUrl) || (await fetchMarkdown()), {
|
||||
copy(markdownCache.get(markdownPageURL) || (await fetchMarkdown()), {
|
||||
onSuccess: () => {
|
||||
// We close the dropdown menu if the action is a dropdown menu item and not the default action.
|
||||
if (type === 'dropdown-menu-item' && !isDefaultAction) {
|
||||
@@ -146,17 +143,17 @@ export function CopyMarkdown(props: {
|
||||
/**
|
||||
* Redirects to the markdown version of the page.
|
||||
*/
|
||||
export function ViewAsMarkdown(props: { markdownPageUrl: string; type: PageActionType }) {
|
||||
const { markdownPageUrl, type } = props;
|
||||
export function ActionViewAsMarkdown(props: { markdownPageURL: string; type: PageActionType }) {
|
||||
const { markdownPageURL, type } = props;
|
||||
const language = useLanguage();
|
||||
|
||||
return (
|
||||
<PageActionWrapper
|
||||
type={type}
|
||||
icon={<MarkdownIcon className="size-4 fill-current" />}
|
||||
icon="markdown"
|
||||
label={tString(language, 'view_page_markdown')}
|
||||
description={tString(language, 'view_page_plaintext')}
|
||||
href={markdownPageUrl}
|
||||
href={markdownPageURL}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -164,7 +161,7 @@ export function ViewAsMarkdown(props: { markdownPageUrl: string; type: PageActio
|
||||
/**
|
||||
* Open the page in a LLM with a pre-filled prompt. Either ChatGPT or Claude.
|
||||
*/
|
||||
export function OpenInLLM(props: {
|
||||
export function ActionOpenInLLM(props: {
|
||||
provider: 'chatgpt' | 'claude';
|
||||
url: string;
|
||||
type: PageActionType;
|
||||
@@ -177,13 +174,7 @@ export function OpenInLLM(props: {
|
||||
return (
|
||||
<PageActionWrapper
|
||||
type={type}
|
||||
icon={
|
||||
provider === 'chatgpt' ? (
|
||||
<ChatGPTIcon className="size-3.5 fill-current" />
|
||||
) : (
|
||||
<ClaudeIcon className="size-3.5 fill-current" />
|
||||
)
|
||||
}
|
||||
icon={provider}
|
||||
label={tString(language, 'open_in', providerLabel)}
|
||||
shortLabel={providerLabel}
|
||||
description={tString(language, 'ai_chat_ask_about_page', providerLabel)}
|
||||
@@ -192,7 +183,10 @@ export function OpenInLLM(props: {
|
||||
);
|
||||
}
|
||||
|
||||
export function GitEditLink(props: {
|
||||
/**
|
||||
* Action to open the page in the associated Git repository.
|
||||
*/
|
||||
export function ActionOpenEditOnGit(props: {
|
||||
type: PageActionType;
|
||||
provider: GitSyncState['installationProvider'];
|
||||
url: string;
|
||||
@@ -214,7 +208,66 @@ export function GitEditLink(props: {
|
||||
);
|
||||
}
|
||||
|
||||
export function ViewAsPDF(props: { url: string; type: PageActionType }) {
|
||||
/**
|
||||
* Action to copy the MCP URL to the clipboard.
|
||||
*/
|
||||
export function ActionCopyMCPURL(props: { mcpURL: string; type: PageActionType }) {
|
||||
const { mcpURL, type } = props;
|
||||
const language = useLanguage();
|
||||
|
||||
return (
|
||||
<CopyToClipboard
|
||||
type={type}
|
||||
data={mcpURL}
|
||||
label={tString(language, 'connect_with_mcp')}
|
||||
description={tString(language, 'copy_mcp_url')}
|
||||
icon="mcp"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to open the MCP server in a specific editor.
|
||||
*/
|
||||
export function ActionOpenMCP(props: {
|
||||
siteTitle: string;
|
||||
mcpURL: string;
|
||||
provider: 'vscode';
|
||||
type: PageActionType;
|
||||
}) {
|
||||
const { siteTitle, provider, mcpURL, type } = props;
|
||||
const language = useLanguage();
|
||||
|
||||
const providerInfo = React.useMemo<{ label: string; icon: IconName; url: string }>(() => {
|
||||
switch (provider) {
|
||||
case 'vscode': {
|
||||
const vscodeConfig = { name: siteTitle, url: mcpURL };
|
||||
return {
|
||||
label: 'VSCode',
|
||||
icon: 'vscode',
|
||||
url: `vscode:mcp/install?${encodeURIComponent(JSON.stringify(vscodeConfig))}`,
|
||||
};
|
||||
}
|
||||
default:
|
||||
assertNever(provider);
|
||||
}
|
||||
}, [provider, mcpURL, siteTitle]);
|
||||
|
||||
return (
|
||||
<PageActionWrapper
|
||||
type={type}
|
||||
href={providerInfo.url}
|
||||
label={tString(language, 'connect_mcp_to', providerInfo.label)}
|
||||
description={tString(language, 'install_mcp_on', providerInfo.label)}
|
||||
icon={providerInfo.icon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to view the page as a PDF.
|
||||
*/
|
||||
export function ActionViewAsPDF(props: { url: string; type: PageActionType }) {
|
||||
const { url, type } = props;
|
||||
const language = useLanguage();
|
||||
|
||||
@@ -229,6 +282,30 @@ export function ViewAsPDF(props: { url: string; type: PageActionType }) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to copy a string to the clipboard.
|
||||
*/
|
||||
export function CopyToClipboard(props: {
|
||||
type: PageActionType;
|
||||
data: string;
|
||||
label: string;
|
||||
description: string;
|
||||
icon: IconName;
|
||||
}) {
|
||||
const { type, data, label, description, icon } = props;
|
||||
return (
|
||||
<PageActionWrapper
|
||||
type={type}
|
||||
icon={icon}
|
||||
label={label}
|
||||
description={description}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(data);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an action in a button (for the default action) or dropdown menu item.
|
||||
*/
|
||||
|
||||
@@ -1,30 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
CopyMarkdown,
|
||||
GitEditLink,
|
||||
OpenAIAssistant,
|
||||
OpenInLLM,
|
||||
ViewAsMarkdown,
|
||||
ViewAsPDF,
|
||||
} from '@/components/PageActions/PageActions';
|
||||
import { Button, ButtonGroup } from '@/components/primitives/Button';
|
||||
import { DropdownMenu, DropdownMenuSeparator } from '@/components/primitives/DropdownMenu';
|
||||
import { tString, useLanguage } from '@/intl/client';
|
||||
import type { GitSyncState, SiteCustomizationSettings } from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { useRef } from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import { useAI } from '../AI';
|
||||
import {
|
||||
ActionCopyMCPURL,
|
||||
ActionCopyMarkdown,
|
||||
ActionOpenAssistant,
|
||||
ActionOpenEditOnGit,
|
||||
ActionOpenInLLM,
|
||||
ActionOpenMCP,
|
||||
ActionViewAsMarkdown,
|
||||
ActionViewAsPDF,
|
||||
} from './PageActions';
|
||||
|
||||
interface PageActionsDropdownProps {
|
||||
markdownPageUrl: string;
|
||||
siteTitle: string;
|
||||
markdownPageURL: string;
|
||||
mcpURL?: string;
|
||||
pdfURL?: string;
|
||||
className?: string;
|
||||
actions: SiteCustomizationSettings['pageActions'];
|
||||
editOnGit?: {
|
||||
provider: GitSyncState['installationProvider'];
|
||||
url: string;
|
||||
};
|
||||
pdfUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,19 +36,14 @@ interface PageActionsDropdownProps {
|
||||
*/
|
||||
export function PageActionsDropdown(props: PageActionsDropdownProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const assistants = useAI().assistants.filter(
|
||||
(assistant) => assistant.ui === true && assistant.pageAction
|
||||
);
|
||||
const language = useLanguage();
|
||||
|
||||
const defaultActions = [assistants.length > 0, props.actions.markdown, props.editOnGit].filter(
|
||||
Boolean
|
||||
);
|
||||
const dropdownActions = [props.actions.externalAI, props.pdfUrl].filter(Boolean);
|
||||
const defaultAction = getPageDefaultAction(props);
|
||||
const dropdownActions = getPageDropdownActions(props);
|
||||
|
||||
return [...defaultActions, ...dropdownActions].length > 0 ? (
|
||||
return defaultAction || dropdownActions.length > 0 ? (
|
||||
<ButtonGroup ref={ref} className={props.className}>
|
||||
{defaultActions.length > 0 ? <DefaultAction {...props} /> : null}
|
||||
{defaultAction}
|
||||
{dropdownActions.length > 0 ? (
|
||||
<DropdownMenu
|
||||
align="end"
|
||||
@@ -57,18 +56,15 @@ export function PageActionsDropdown(props: PageActionsDropdownProps) {
|
||||
className="size-3 transition-transform group-data-[state=open]/button:rotate-180"
|
||||
/>
|
||||
}
|
||||
label={tString(
|
||||
language,
|
||||
defaultActions.some(Boolean) ? 'more' : 'actions'
|
||||
)}
|
||||
iconOnly={defaultActions.some(Boolean)}
|
||||
label={tString(language, defaultAction ? 'more' : 'actions')}
|
||||
iconOnly={!!defaultAction}
|
||||
size="xsmall"
|
||||
variant="secondary"
|
||||
className="bg-tint-base text-sm"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<PageActionsDropdownMenuContent {...props} />
|
||||
{dropdownActions}
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
</ButtonGroup>
|
||||
@@ -76,80 +72,99 @@ export function PageActionsDropdown(props: PageActionsDropdownProps) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The content of the dropdown menu.
|
||||
* Return the list of actions to show in the dropdown menu.
|
||||
*/
|
||||
function PageActionsDropdownMenuContent(props: PageActionsDropdownProps) {
|
||||
const { markdownPageUrl, actions } = props;
|
||||
function getPageDropdownActions(props: PageActionsDropdownProps): React.ReactNode[] {
|
||||
const { siteTitle, markdownPageURL, mcpURL, actions } = props;
|
||||
const assistants = useAI().assistants.filter(
|
||||
(assistant) => assistant.ui === true && assistant.pageAction
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{assistants.map((assistant) => (
|
||||
<OpenAIAssistant
|
||||
key={assistant.label}
|
||||
assistant={assistant}
|
||||
return [
|
||||
...assistants.map((assistant) => (
|
||||
<ActionOpenAssistant
|
||||
key={assistant.label}
|
||||
assistant={assistant}
|
||||
type="dropdown-menu-item"
|
||||
/>
|
||||
)),
|
||||
|
||||
actions.markdown ? (
|
||||
<React.Fragment key="markdown">
|
||||
<DropdownMenuSeparator className="first:hidden" />
|
||||
<ActionCopyMarkdown
|
||||
isDefaultAction={!assistants.length}
|
||||
markdownPageURL={markdownPageURL}
|
||||
type="dropdown-menu-item"
|
||||
/>
|
||||
))}
|
||||
<ActionViewAsMarkdown markdownPageURL={markdownPageURL} type="dropdown-menu-item" />
|
||||
</React.Fragment>
|
||||
) : null,
|
||||
|
||||
{actions.markdown ? (
|
||||
<>
|
||||
<DropdownMenuSeparator className="first:hidden" />
|
||||
<CopyMarkdown
|
||||
isDefaultAction={!assistants.length}
|
||||
markdownPageUrl={markdownPageUrl}
|
||||
actions.externalAI ? (
|
||||
<React.Fragment key="externalAI">
|
||||
<DropdownMenuSeparator className="first:hidden" />
|
||||
<ActionOpenInLLM
|
||||
provider="chatgpt"
|
||||
url={markdownPageURL}
|
||||
type="dropdown-menu-item"
|
||||
/>
|
||||
<ActionOpenInLLM
|
||||
provider="claude"
|
||||
url={markdownPageURL}
|
||||
type="dropdown-menu-item"
|
||||
/>
|
||||
</React.Fragment>
|
||||
) : null,
|
||||
|
||||
actions.mcp && mcpURL ? (
|
||||
<React.Fragment key="mcp">
|
||||
<DropdownMenuSeparator className="first:hidden" />
|
||||
<ActionCopyMCPURL mcpURL={mcpURL} type="dropdown-menu-item" />
|
||||
<ActionOpenMCP
|
||||
provider="vscode"
|
||||
mcpURL={mcpURL}
|
||||
siteTitle={siteTitle}
|
||||
type="dropdown-menu-item"
|
||||
/>
|
||||
</React.Fragment>
|
||||
) : null,
|
||||
|
||||
props.editOnGit || props.pdfURL ? (
|
||||
<React.Fragment key="editOnGit">
|
||||
<DropdownMenuSeparator className="first:hidden" />
|
||||
{props.editOnGit ? (
|
||||
<ActionOpenEditOnGit
|
||||
type="dropdown-menu-item"
|
||||
provider={props.editOnGit.provider}
|
||||
url={props.editOnGit.url}
|
||||
/>
|
||||
<ViewAsMarkdown markdownPageUrl={markdownPageUrl} type="dropdown-menu-item" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{actions.externalAI ? (
|
||||
<>
|
||||
<DropdownMenuSeparator className="first:hidden" />
|
||||
<OpenInLLM provider="chatgpt" url={markdownPageUrl} type="dropdown-menu-item" />
|
||||
<OpenInLLM provider="claude" url={markdownPageUrl} type="dropdown-menu-item" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{props.editOnGit || props.pdfUrl ? (
|
||||
<>
|
||||
<DropdownMenuSeparator className="first:hidden" />
|
||||
{props.editOnGit ? (
|
||||
<GitEditLink
|
||||
type="dropdown-menu-item"
|
||||
provider={props.editOnGit.provider}
|
||||
url={props.editOnGit.url}
|
||||
/>
|
||||
) : null}
|
||||
{props.pdfUrl ? (
|
||||
<ViewAsPDF url={props.pdfUrl} type="dropdown-menu-item" />
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
) : null}
|
||||
{props.pdfURL ? (
|
||||
<ActionViewAsPDF url={props.pdfURL} type="dropdown-menu-item" />
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
) : null,
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* A default action shown as a quick-access button beside the dropdown menu
|
||||
*/
|
||||
function DefaultAction(props: PageActionsDropdownProps) {
|
||||
const { markdownPageUrl, actions } = props;
|
||||
function getPageDefaultAction(props: PageActionsDropdownProps) {
|
||||
const { markdownPageURL, actions } = props;
|
||||
const assistants = useAI().assistants.filter(
|
||||
(assistant) => assistant.ui === true && assistant.pageAction
|
||||
);
|
||||
|
||||
const assistant = assistants[0];
|
||||
if (assistant) {
|
||||
return <OpenAIAssistant assistant={assistant} type="button" />;
|
||||
return <ActionOpenAssistant assistant={assistant} type="button" />;
|
||||
}
|
||||
|
||||
if (props.editOnGit) {
|
||||
return (
|
||||
<GitEditLink
|
||||
<ActionOpenEditOnGit
|
||||
type="button"
|
||||
provider={props.editOnGit.provider}
|
||||
url={props.editOnGit.url}
|
||||
@@ -159,11 +174,13 @@ function DefaultAction(props: PageActionsDropdownProps) {
|
||||
|
||||
if (actions.markdown) {
|
||||
return (
|
||||
<CopyMarkdown
|
||||
<ActionCopyMarkdown
|
||||
isDefaultAction={!assistant}
|
||||
markdownPageUrl={markdownPageUrl}
|
||||
markdownPageURL={markdownPageURL}
|
||||
type="button"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
export function ChatGPTIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 320" {...props}>
|
||||
<title>ChatGPT</title>
|
||||
<path d="m297.06 130.97c7.26-21.79 4.76-45.66-6.85-65.48-17.46-30.4-52.56-46.04-86.84-38.68-15.25-17.18-37.16-26.95-60.13-26.81-35.04-.08-66.13 22.48-76.91 55.82-22.51 4.61-41.94 18.7-53.31 38.67-17.59 30.32-13.58 68.54 9.92 94.54-7.26 21.79-4.76 45.66 6.85 65.48 17.46 30.4 52.56 46.04 86.84 38.68 15.24 17.18 37.16 26.95 60.13 26.8 35.06.09 66.16-22.49 76.94-55.86 22.51-4.61 41.94-18.7 53.31-38.67 17.57-30.32 13.55-68.51-9.94-94.51zm-120.28 168.11c-14.03.02-27.62-4.89-38.39-13.88.49-.26 1.34-.73 1.89-1.07l63.72-36.8c3.26-1.85 5.26-5.32 5.24-9.07v-89.83l26.93 15.55c.29.14.48.42.52.74v74.39c-.04 33.08-26.83 59.9-59.91 59.97zm-128.84-55.03c-7.03-12.14-9.56-26.37-7.15-40.18.47.28 1.3.79 1.89 1.13l63.72 36.8c3.23 1.89 7.23 1.89 10.47 0l77.79-44.92v31.1c.02.32-.13.63-.38.83l-64.41 37.19c-28.69 16.52-65.33 6.7-81.92-21.95zm-16.77-139.09c7-12.16 18.05-21.46 31.21-26.29 0 .55-.03 1.52-.03 2.2v73.61c-.02 3.74 1.98 7.21 5.23 9.06l77.79 44.91-26.93 15.55c-.27.18-.61.21-.91.08l-64.42-37.22c-28.63-16.58-38.45-53.21-21.95-81.89zm221.26 51.49-77.79-44.92 26.93-15.54c.27-.18.61-.21.91-.08l64.42 37.19c28.68 16.57 38.51 53.26 21.94 81.94-7.01 12.14-18.05 21.44-31.2 26.28v-75.81c.03-3.74-1.96-7.2-5.2-9.06zm26.8-40.34c-.47-.29-1.3-.79-1.89-1.13l-63.72-36.8c-3.23-1.89-7.23-1.89-10.47 0l-77.79 44.92v-31.1c-.02-.32.13-.63.38-.83l64.41-37.16c28.69-16.55 65.37-6.7 81.91 22 6.99 12.12 9.52 26.31 7.15 40.1zm-168.51 55.43-26.94-15.55c-.29-.14-.48-.42-.52-.74v-74.39c.02-33.12 26.89-59.96 60.01-59.94 14.01 0 27.57 4.92 38.34 13.88-.49.26-1.33.73-1.89 1.07l-63.72 36.8c-3.26 1.85-5.26 5.31-5.24 9.06l-.04 89.79zm14.63-31.54 34.65-20.01 34.65 20v40.01l-34.65 20-34.65-20z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
export function ClaudeIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 256 257"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
{...props}
|
||||
>
|
||||
<title>Claude</title>
|
||||
<g>
|
||||
<path
|
||||
d="M50.2278481,170.321013 L100.585316,142.063797 L101.427848,139.601013 L100.585316,138.24 L98.1225316,138.24 L89.6972152,137.721519 L60.921519,136.943797 L35.9696203,135.906835 L11.795443,134.610633 L5.70329114,133.31443 L0,125.796456 L0.583291139,122.037468 L5.70329114,118.602532 L13.0268354,119.250633 L29.2293671,120.352405 L53.5331646,122.037468 L71.161519,123.07443 L97.28,125.796456 L101.427848,125.796456 L102.011139,124.111392 L100.585316,123.07443 L99.4835443,122.037468 L74.3372152,104.992405 L47.116962,86.9751899 L32.8587342,76.6055696 L25.1463291,71.3559494 L21.2577215,66.4303797 L19.5726582,55.6718987 L26.5721519,47.9594937 L35.9696203,48.6075949 L38.3675949,49.2556962 L47.8946835,56.5792405 L68.2450633,72.3281013 L94.8172152,91.9007595 L98.7058228,95.1412658 L100.261266,94.0394937 L100.455696,93.2617722 L98.7058228,90.3453165 L84.2531646,64.2268354 L68.8283544,37.6546835 L61.958481,26.636962 L60.1437975,20.0263291 C59.4956962,17.3043038 59.0420253,15.0359494 59.0420253,12.2491139 L67.0136709,1.42582278 L71.4207595,-1.42108547e-14 L82.0496203,1.42582278 L86.521519,5.31443038 L93.1321519,20.4151899 L103.825823,44.2005063 L120.417215,76.5407595 L125.277975,86.1326582 L127.87038,95.0116456 L128.842532,97.7336709 L130.527595,97.7336709 L130.527595,96.1782278 L131.888608,77.9665823 L134.416203,55.6070886 L136.878987,26.8313924 L137.721519,18.7301266 L141.739747,9.00860759 L149.711392,3.75898734 L155.933165,6.74025316 L161.053165,14.0637975 L160.340253,18.7949367 L157.294177,38.5620253 L151.331646,69.5412658 L147.443038,90.2805063 L149.711392,90.2805063 L152.303797,87.6881013 L162.803038,73.7539241 L180.431392,51.718481 L188.208608,42.9691139 L197.282025,33.3124051 L203.114937,28.7108861 L214.132658,28.7108861 L222.233924,40.7655696 L218.604557,53.2091139 L207.262785,67.596962 L197.865316,79.7812658 L184.38481,97.9281013 L175.959494,112.44557 L176.737215,113.612152 L178.746329,113.417722 L209.207089,106.936709 L225.668861,103.955443 L245.306329,100.585316 L254.185316,104.733165 L255.157468,108.945823 L251.657722,117.56557 L230.659241,122.75038 L206.031392,127.675949 L169.348861,136.360506 L168.89519,136.684557 L169.413671,137.332658 L185.940253,138.888101 L193.004557,139.276962 L210.308861,139.276962 L242.519494,141.674937 L250.94481,147.248608 L256,154.053671 L255.157468,159.238481 L242.195443,165.849114 L224.696709,161.701266 L183.866329,151.979747 L169.867342,148.48 L167.923038,148.48 L167.923038,149.646582 L179.588861,161.053165 L200.976203,180.366582 L227.742785,205.253671 L229.103797,211.410633 L225.668861,216.271392 L222.039494,215.752911 L198.513418,198.059747 L189.44,190.088101 L168.89519,172.783797 L167.534177,172.783797 L167.534177,174.598481 L172.265316,181.533165 L197.282025,219.123038 L198.578228,230.659241 L196.763544,234.418228 L190.282532,236.686582 L183.153418,235.39038 L168.506329,214.84557 L153.40557,191.708354 L141.221266,170.969114 L139.730633,171.811646 L132.536709,249.259747 L129.166582,253.213165 L121.389367,256.19443 L114.908354,251.268861 L111.473418,243.297215 L114.908354,227.548354 L119.056203,207.003544 L122.426329,190.671392 L125.472405,170.385823 L127.287089,163.64557 L127.157468,163.191899 L125.666835,163.386329 L110.371646,184.38481 L87.1048101,215.817722 L68.6987342,235.52 L64.2916456,237.269873 L56.6440506,233.316456 L57.356962,226.252152 L61.6344304,219.96557 L87.1048101,187.560506 L102.46481,167.469367 L112.380759,155.868354 L112.315949,154.183291 L111.732658,154.183291 L44.0708861,198.124557 L32.0162025,199.68 L26.8313924,194.819241 L27.4794937,186.847595 L29.9422785,184.25519 L50.2926582,170.256203 L50.2278481,170.321013 Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export function MarkdownIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 471 289.85" {...props}>
|
||||
<title>Markdown</title>
|
||||
<path d="M437,289.85H34a34,34,0,0,1-34-34V34A34,34,0,0,1,34,0H437a34,34,0,0,1,34,34V255.88A34,34,0,0,1,437,289.85ZM34,22.64A11.34,11.34,0,0,0,22.64,34V255.88A11.34,11.34,0,0,0,34,267.2H437a11.34,11.34,0,0,0,11.33-11.32V34A11.34,11.34,0,0,0,437,22.64Z" />
|
||||
<path d="M67.93,221.91v-154h45.29l45.29,56.61L203.8,67.93h45.29v154H203.8V133.6l-45.29,56.61L113.22,133.6v88.31Zm283.06,0-67.94-74.72h45.29V67.93h45.29v79.26h45.29Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import type { AncestorRevisionPage } from '@/lib/pages';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import type { RevisionPageDocument } from '@gitbook/api';
|
||||
import { type RevisionPageDocument, SiteVisibility } from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import urlJoin from 'url-join';
|
||||
import { getPDFURLSearchParams } from '../PDF';
|
||||
@@ -39,7 +39,8 @@ export async function PageHeader(props: {
|
||||
{page.layout.tableOfContents ? (
|
||||
// Show page actions if *any* of the actions are enabled
|
||||
<PageActionsDropdown
|
||||
markdownPageUrl={`${context.linker.toAbsoluteURL(context.linker.toPathInSpace(page.path))}.md`}
|
||||
siteTitle={context.site.title}
|
||||
markdownPageURL={`${context.linker.toAbsoluteURL(context.linker.toPathInSpace(page.path))}.md`}
|
||||
editOnGit={
|
||||
context.customization.git.showEditLink &&
|
||||
context.space.gitSync?.url &&
|
||||
@@ -50,7 +51,7 @@ export async function PageHeader(props: {
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
pdfUrl={
|
||||
pdfURL={
|
||||
context.customization.pdf.enabled
|
||||
? context.linker.toPathInSpace(
|
||||
`~gitbook/pdf?${getPDFURLSearchParams({
|
||||
@@ -61,6 +62,13 @@ export async function PageHeader(props: {
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
mcpURL={
|
||||
context.site.visibility !== SiteVisibility.VisitorAuth
|
||||
? context.linker.toAbsoluteURL(
|
||||
context.linker.toPathInSpace('~gitbook/mcp')
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
actions={context.customization.pageActions}
|
||||
className={tcls(
|
||||
'float-right ml-4 xl:max-2xl:page-api-block:mr-62',
|
||||
|
||||
@@ -123,8 +123,7 @@ export async function streamAskQuestion({
|
||||
},
|
||||
scope: {
|
||||
mode: 'default',
|
||||
// Include the current site space regardless.
|
||||
includedSiteSpaces: [context.siteSpace.id],
|
||||
currentSiteSpace: context.siteSpace.id,
|
||||
},
|
||||
},
|
||||
{ format: 'document' }
|
||||
|
||||
@@ -30,9 +30,7 @@ export function encodeClientSiteSections(context: GitBookSiteContext, sections:
|
||||
for (const item of list) {
|
||||
switch (item.object) {
|
||||
case 'site-section-group': {
|
||||
const sections = item.sections
|
||||
.filter((section) => shouldIncludeSection(context, section))
|
||||
.map((section) => encodeSection(context, section));
|
||||
const sections = item.sections.map((section) => encodeSection(context, section));
|
||||
|
||||
// Skip empty groups
|
||||
if (sections.length === 0) {
|
||||
@@ -74,38 +72,13 @@ function encodeSection(context: GitBookSiteContext, section: SiteSection) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a section should be included in the list of sections.
|
||||
*/
|
||||
function shouldIncludeSection(context: GitBookSiteContext, section: SiteSection) {
|
||||
if (context.site.id !== 'site_JOVzv') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Testing for a new mode of navigation where the multi-variants section are hidden
|
||||
// if they do not include an equivalent of the current site space.
|
||||
|
||||
// TODO: replace with a proper flag on the section
|
||||
const withNavigateOnlyIfEquivalent = section.id === 'sitesc_4jvEm';
|
||||
|
||||
if (!withNavigateOnlyIfEquivalent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { siteSpace: currentSiteSpace } = context;
|
||||
if (section.siteSpaces.length === 1) {
|
||||
return true;
|
||||
}
|
||||
return section.siteSpaces.some((siteSpace) =>
|
||||
areSiteSpacesEquivalent(siteSpace, currentSiteSpace)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the best default site space to navigate to for a givent section:
|
||||
* 1. If we are on the default, continue on the default.
|
||||
* 2. If a site space has the same path as the current one, return it.
|
||||
* 3. Otherwise, return the default one.
|
||||
* 2. If there are site spaces with the same language as the current, filter by language.
|
||||
* 3. If a site space has the same path as the current one, return it.
|
||||
* 4. Otherwise, return the default first language match.
|
||||
* 5. Otherwise, return the default one.
|
||||
*/
|
||||
function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) {
|
||||
const { siteSpace: currentSiteSpace } = context;
|
||||
@@ -114,9 +87,15 @@ function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) {
|
||||
return getSectionURL(context, section);
|
||||
}
|
||||
|
||||
const bestMatch = section.siteSpaces.find((siteSpace) =>
|
||||
areSiteSpacesEquivalent(siteSpace, currentSiteSpace)
|
||||
);
|
||||
const possibleMatches =
|
||||
section.siteSpaces.filter((siteSpace) =>
|
||||
areSiteSpacesSameLanguage(siteSpace, currentSiteSpace)
|
||||
) ?? section.siteSpaces;
|
||||
|
||||
const bestMatch =
|
||||
possibleMatches.find((siteSpace) => areSiteSpacesEquivalent(siteSpace, currentSiteSpace)) ??
|
||||
possibleMatches[0];
|
||||
|
||||
if (bestMatch) {
|
||||
return getSiteSpaceURL(context, bestMatch);
|
||||
}
|
||||
@@ -130,3 +109,7 @@ function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) {
|
||||
function areSiteSpacesEquivalent(siteSpace1: SiteSpace, siteSpace2: SiteSpace) {
|
||||
return siteSpace1.path === siteSpace2.path;
|
||||
}
|
||||
|
||||
function areSiteSpacesSameLanguage(siteSpace1: SiteSpace, siteSpace2: SiteSpace) {
|
||||
return siteSpace1.space.language === siteSpace2.space.language;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import type { RenderAIMessageOptions } from '../AI';
|
||||
import { AIChat } from '../AIChat';
|
||||
import { AdaptiveVisitorContextProvider } from '../Adaptive';
|
||||
import { Announcement } from '../Announcement';
|
||||
import { SpacesDropdown } from '../Header/SpacesDropdown';
|
||||
import { SpacesDropdown, TranslationsDropdown } from '../Header/SpacesDropdown';
|
||||
import { InsightsProvider } from '../Insights';
|
||||
import { SearchContainer } from '../Search';
|
||||
import { SiteSectionList, encodeClientSiteSections } from '../SiteSections';
|
||||
@@ -156,12 +156,20 @@ export function SpaceLayout(props: SpaceLayoutProps) {
|
||||
'pr-4',
|
||||
'lg:flex',
|
||||
'grow-0',
|
||||
'flex-wrap',
|
||||
'dark:shadow-light/1',
|
||||
'text-base/tight'
|
||||
'text-base/tight',
|
||||
'items-center'
|
||||
)}
|
||||
>
|
||||
<HeaderLogo context={context} />
|
||||
{withVariants === 'translations' ? (
|
||||
<TranslationsDropdown
|
||||
context={context}
|
||||
siteSpace={siteSpace}
|
||||
siteSpaces={siteSpaces}
|
||||
className="[&_.button-leading-icon]:block! ml-auto py-2 [&_.button-content]:hidden"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ export const de = {
|
||||
pdf_limit_reached_continue: 'Mit ${1} weiteren Seiten erweitern.',
|
||||
more: 'Mehr',
|
||||
link_tooltip_external_link: 'Externe Verlinkung zu',
|
||||
link_tooltip_email: 'E-Mail senden an',
|
||||
link_tooltip_page_anchor: 'Zum Abschnitt springen',
|
||||
open_in_new_tab: 'In neuem Tab öffnen',
|
||||
ai_chat_assistant_name: 'GitBook-Assistent',
|
||||
@@ -111,6 +112,10 @@ export const de = {
|
||||
view_page_plaintext: 'Diese Seite als Klartext anzeigen',
|
||||
open_in: 'Öffnen in ${1}',
|
||||
open_in_llms_pre_prompt: 'Lies ${1} und beantworte Fragen zum Inhalt.',
|
||||
connect_mcp_to: 'Mit ${1} verbinden',
|
||||
install_mcp_on: 'MCP-Server auf ${1} installieren',
|
||||
connect_with_mcp: 'Mit MCP verbinden',
|
||||
copy_mcp_url: 'MCP-Server-URL kopieren',
|
||||
press_to_confirm: 'Drücke ${1} zum Bestätigen',
|
||||
tool_call_skipped: 'Übersprungen "${1}"',
|
||||
};
|
||||
|
||||
@@ -65,6 +65,7 @@ export const en = {
|
||||
pdf_limit_reached_continue: 'Extend with ${1} more pages.',
|
||||
more: 'More',
|
||||
link_tooltip_external_link: 'External link to',
|
||||
link_tooltip_email: 'Send an email to',
|
||||
link_tooltip_page_anchor: 'Jump to section',
|
||||
open_in_new_tab: 'Open in new tab',
|
||||
ai_chat_assistant_name: 'GitBook Assistant',
|
||||
@@ -108,6 +109,10 @@ export const en = {
|
||||
view_page_plaintext: 'View this page as plain text',
|
||||
open_in: 'Open in ${1}',
|
||||
open_in_llms_pre_prompt: 'Read ${1} and answer questions about the content.',
|
||||
connect_mcp_to: 'Connect to ${1}',
|
||||
install_mcp_on: 'Install MCP Server on ${1}',
|
||||
connect_with_mcp: 'Connect with MCP',
|
||||
copy_mcp_url: 'Copy the MCP Server URL',
|
||||
press_to_confirm: 'Press ${1} to confirm',
|
||||
tool_call_skipped: 'Skipped "${1}"',
|
||||
};
|
||||
|
||||
@@ -69,6 +69,7 @@ export const es: TranslationLanguage = {
|
||||
pdf_limit_reached_continue: 'Extender con ${1} páginas más.',
|
||||
more: 'Más',
|
||||
link_tooltip_external_link: 'Enlace externo a',
|
||||
link_tooltip_email: 'Enviar un correo electrónico a',
|
||||
link_tooltip_page_anchor: 'Saltar a la sección',
|
||||
open_in_new_tab: 'Abrir en una nueva pestaña',
|
||||
ai_chat_assistant_name: 'Asistente de GitBook',
|
||||
@@ -112,6 +113,10 @@ export const es: TranslationLanguage = {
|
||||
view_page_plaintext: 'Ver esta página como texto plano',
|
||||
open_in: 'Abrir en ${1}',
|
||||
open_in_llms_pre_prompt: 'Lee ${1} y responde preguntas sobre el contenido.',
|
||||
connect_mcp_to: 'Conectar a ${1}',
|
||||
install_mcp_on: 'Instalar servidor MCP en ${1}',
|
||||
connect_with_mcp: 'Conectar con MCP',
|
||||
copy_mcp_url: 'Copiar URL del servidor MCP',
|
||||
press_to_confirm: 'Presiona ${1} para confirmar',
|
||||
tool_call_skipped: 'Omitido "${1}"',
|
||||
};
|
||||
|
||||
@@ -64,6 +64,7 @@ export const fr = {
|
||||
pdf_limit_reached_continue: 'Ajouter ${1} pages supplémentaires',
|
||||
more: 'Plus',
|
||||
link_tooltip_external_link: 'Lien externe vers',
|
||||
link_tooltip_email: 'Envoyer un e-mail à',
|
||||
link_tooltip_page_anchor: 'Aller à la section',
|
||||
open_in_new_tab: 'Ouvrir dans un nouvel onglet',
|
||||
ai_chat_assistant_name: 'Assistant GitBook',
|
||||
@@ -107,6 +108,10 @@ export const fr = {
|
||||
view_page_plaintext: 'Voir en texte brut',
|
||||
open_in: 'Ouvrir dans ${1}',
|
||||
open_in_llms_pre_prompt: 'Lire ${1} et répondre aux questions sur le contenu.',
|
||||
connect_mcp_to: 'Se connecter à ${1}',
|
||||
install_mcp_on: 'Installer le serveur MCP sur ${1}',
|
||||
connect_with_mcp: 'Se connecter avec MCP',
|
||||
copy_mcp_url: "Copier l'URL du serveur MCP",
|
||||
press_to_confirm: 'Appuyez sur ${1} pour confirmer',
|
||||
tool_call_skipped: 'Ignoré "${1}"',
|
||||
};
|
||||
|
||||
@@ -67,6 +67,7 @@ export const ja: TranslationLanguage = {
|
||||
pdf_limit_reached_continue: 'さらに${1}ページで拡張',
|
||||
more: '詳細',
|
||||
link_tooltip_external_link: '外部リンク先',
|
||||
link_tooltip_email: 'メールを送信',
|
||||
link_tooltip_page_anchor: 'ページ内リンク先',
|
||||
open_in_new_tab: '新しいタブで開く',
|
||||
ai_chat_assistant_name: 'GitBookアシスタント',
|
||||
@@ -110,6 +111,10 @@ export const ja: TranslationLanguage = {
|
||||
view_page_plaintext: 'このページをプレーンテキストで表示',
|
||||
open_in: '${1} で開く',
|
||||
open_in_llms_pre_prompt: '${1} を読んで内容に関する質問に答えてください。',
|
||||
connect_mcp_to: '${1} に接続',
|
||||
install_mcp_on: '${1} にMCPサーバーをインストール',
|
||||
connect_with_mcp: 'MCPに接続',
|
||||
copy_mcp_url: 'MCPサーバーのURLをコピー',
|
||||
press_to_confirm: '確認するには${1}を押してください',
|
||||
tool_call_skipped: '"${1}" をスキップしました',
|
||||
};
|
||||
|
||||
@@ -67,6 +67,7 @@ export const nl: TranslationLanguage = {
|
||||
pdf_limit_reached_continue: "Verleng met ${1} extra pagina's.",
|
||||
more: 'Meer',
|
||||
link_tooltip_external_link: 'Externe link naar',
|
||||
link_tooltip_email: 'E-mail versturen naar',
|
||||
link_tooltip_page_anchor: 'Spring naar sectie',
|
||||
open_in_new_tab: 'Open in nieuw tabblad',
|
||||
ai_chat_assistant_name: 'GitBook Assistent',
|
||||
@@ -110,6 +111,10 @@ export const nl: TranslationLanguage = {
|
||||
view_page_plaintext: 'Bekijk deze pagina als platte tekst',
|
||||
open_in: 'Open in ${1}',
|
||||
open_in_llms_pre_prompt: 'Lees ${1} en beantwoord vragen over de inhoud.',
|
||||
connect_mcp_to: 'Verbind met ${1}',
|
||||
install_mcp_on: 'Installeer MCP-server op ${1}',
|
||||
connect_with_mcp: 'Verbind met MCP',
|
||||
copy_mcp_url: 'Kopieer MCP-server URL',
|
||||
press_to_confirm: 'Druk op ${1} om te bevestigen',
|
||||
tool_call_skipped: '"${1}" overgeslagen',
|
||||
};
|
||||
|
||||
@@ -68,6 +68,7 @@ export const no: TranslationLanguage = {
|
||||
pdf_limit_reached_continue: 'Utvid med ${1} flere sider.',
|
||||
more: 'Mer',
|
||||
link_tooltip_external_link: 'Ekstern lenke til',
|
||||
link_tooltip_email: 'Send e-post til',
|
||||
link_tooltip_page_anchor: 'Hopp til seksjon',
|
||||
open_in_new_tab: 'Åpne i ny fane',
|
||||
ai_chat_assistant_name: 'GitBook-assistent',
|
||||
@@ -111,6 +112,10 @@ export const no: TranslationLanguage = {
|
||||
view_page_plaintext: 'Vis denne siden som ren tekst',
|
||||
open_in: 'Åpne i ${1}',
|
||||
open_in_llms_pre_prompt: 'Les ${1} og besvar spørsmål om innholdet.',
|
||||
connect_mcp_to: 'Koble til ${1}',
|
||||
install_mcp_on: 'Installer MCP-server på ${1}',
|
||||
connect_with_mcp: 'Koble til MCP',
|
||||
copy_mcp_url: 'Kopier MCP-server URL',
|
||||
press_to_confirm: 'Trykk ${1} for å bekrefte',
|
||||
tool_call_skipped: 'Hoppet over "${1}"',
|
||||
};
|
||||
|
||||
@@ -67,6 +67,7 @@ export const pt_br = {
|
||||
pdf_limit_reached_continue: 'Extender com mais ${1} páginas.',
|
||||
more: 'Mais',
|
||||
link_tooltip_external_link: 'Link externo para',
|
||||
link_tooltip_email: 'Enviar e-mail para',
|
||||
link_tooltip_page_anchor: 'Pular para a seção',
|
||||
open_in_new_tab: 'Abrir em uma nova guia',
|
||||
ai_chat_assistant_name: 'Assistente do GitBook',
|
||||
@@ -110,6 +111,10 @@ export const pt_br = {
|
||||
view_page_plaintext: 'Ver esta página em texto simples',
|
||||
open_in: 'Abrir no ${1}',
|
||||
open_in_llms_pre_prompt: 'Leia ${1} e responda perguntas sobre o conteúdo.',
|
||||
connect_mcp_to: 'Conectar a ${1}',
|
||||
install_mcp_on: 'Instalar servidor MCP em ${1}',
|
||||
connect_with_mcp: 'Conectar com MCP',
|
||||
copy_mcp_url: 'Copiar URL do servidor MCP',
|
||||
press_to_confirm: 'Pressione ${1} para confirmar',
|
||||
tool_call_skipped: 'Pulado "${1}"',
|
||||
};
|
||||
|
||||
@@ -66,6 +66,7 @@ export const ru = {
|
||||
pdf_limit_reached_continue: 'Расширьте ещё на ${1} страниц.',
|
||||
more: 'Ещё',
|
||||
link_tooltip_external_link: 'Внешняя ссылка на',
|
||||
link_tooltip_email: 'Отправить письмо на',
|
||||
link_tooltip_page_anchor: 'Перейти к разделу',
|
||||
open_in_new_tab: 'Открыть в новой вкладке',
|
||||
ai_chat_assistant_name: 'GitBook-помощник',
|
||||
@@ -109,6 +110,10 @@ export const ru = {
|
||||
view_page_plaintext: 'Посмотреть как текст без форматирования',
|
||||
open_in: 'Открыть в ${1}',
|
||||
open_in_llms_pre_prompt: 'Прочитай ${1} и ответь на вопросы о содержимом.',
|
||||
connect_mcp_to: 'Подключиться к ${1}',
|
||||
install_mcp_on: 'Установить MCP-сервер на ${1}',
|
||||
connect_with_mcp: 'Подключиться к MCP',
|
||||
copy_mcp_url: 'Скопировать URL MCP-сервера',
|
||||
press_to_confirm: 'Нажмите ${1} для подтверждения',
|
||||
tool_call_skipped: 'Пропущен "${1}"',
|
||||
};
|
||||
|
||||
@@ -65,6 +65,7 @@ export const zh: TranslationLanguage = {
|
||||
pdf_limit_reached_continue: '使用${1}页进行扩展。',
|
||||
more: '更多',
|
||||
link_tooltip_external_link: '外部链接到',
|
||||
link_tooltip_email: '发送邮件到',
|
||||
link_tooltip_page_anchor: '跳转到页面',
|
||||
open_in_new_tab: '在新标签页中打开',
|
||||
ai_chat_assistant_name: 'GitBook 助手',
|
||||
@@ -107,6 +108,10 @@ export const zh: TranslationLanguage = {
|
||||
view_page_plaintext: '以纯文本查看此页面',
|
||||
open_in: '在 ${1} 中打开',
|
||||
open_in_llms_pre_prompt: '阅读 ${1} 并回答内容相关的问题。',
|
||||
connect_mcp_to: '连接到 ${1}',
|
||||
install_mcp_on: '在 ${1} 上安装 MCP 服务器',
|
||||
connect_with_mcp: '连接 MCP',
|
||||
copy_mcp_url: '复制 MCP 服务器 URL',
|
||||
press_to_confirm: '按 ${1} 确认',
|
||||
tool_call_skipped: '已跳过 "${1}"',
|
||||
};
|
||||
|
||||
@@ -67,6 +67,7 @@ export function defaultCustomization(): api.SiteCustomizationSettings {
|
||||
pageActions: {
|
||||
externalAI: true,
|
||||
markdown: true,
|
||||
mcp: true,
|
||||
},
|
||||
trademark: {
|
||||
enabled: true,
|
||||
|
||||
@@ -557,6 +557,7 @@ function encodePathInSiteContent(rawPathname: string): {
|
||||
case '~gitbook/embed/assistant':
|
||||
case '~gitbook/icon':
|
||||
return { pathname };
|
||||
case '~gitbook/mcp':
|
||||
case 'llms.txt':
|
||||
case 'sitemap.xml':
|
||||
case 'sitemap-pages.xml':
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { expect, it } from 'bun:test';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client';
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||
import { getContentTestURL } from './utils';
|
||||
|
||||
it('should expose a MCP server', async () => {
|
||||
const client = new Client({
|
||||
name: 'test',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
await client.connect(
|
||||
new StreamableHTTPClientTransport(
|
||||
new URL(getContentTestURL('https://gitbook.com/docs/~gitbook/mcp'))
|
||||
)
|
||||
);
|
||||
|
||||
const tools = await client.listTools();
|
||||
expect(tools.tools[0]?.name).toBe('searchDocumentation');
|
||||
|
||||
const response = await client.callTool({
|
||||
name: 'searchDocumentation',
|
||||
arguments: {
|
||||
query: 'git',
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-expect-error - response.content is of type unknown
|
||||
expect(response.content[0]?.text).toContain('Title:');
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
import psi from 'psi';
|
||||
|
||||
import { getContentTestURL } from './utils';
|
||||
|
||||
interface Test {
|
||||
url: string;
|
||||
strategy: 'mobile' | 'desktop';
|
||||
threshold: number;
|
||||
}
|
||||
|
||||
// We use low values for now to avoid failing the tests
|
||||
// and to be able to see the results, and only catch major regressions.
|
||||
const tests: Array<Test> = [
|
||||
{
|
||||
url: 'https://gitbook.com/docs',
|
||||
strategy: 'desktop',
|
||||
threshold: 60,
|
||||
},
|
||||
|
||||
{
|
||||
url: 'https://gitbook.com/docs',
|
||||
strategy: 'mobile',
|
||||
threshold: 30,
|
||||
},
|
||||
];
|
||||
|
||||
for (const test of tests) {
|
||||
const url = getContentTestURL(test.url);
|
||||
await psi.output(url, {
|
||||
strategy: test.strategy,
|
||||
threshold: test.threshold,
|
||||
key: process.env.PAGESPEED_API_KEY,
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
# @gitbook/icons
|
||||
|
||||
## 0.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 262afa3: Fix types for custom icons and update list of icons
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
"default": "./src/style.css"
|
||||
}
|
||||
},
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||
"@fortawesome/fontawesome-free": "^6.6.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@gitbook/fontawesome-pro": "1.0.11"
|
||||
"@gitbook/fontawesome-pro": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3"
|
||||
|
||||
@@ -18,6 +18,13 @@ export enum IconStyle {
|
||||
SharpThin = 'sharp-thin',
|
||||
}
|
||||
|
||||
type CustomIconName = 'gitbook';
|
||||
type CustomIconName =
|
||||
| 'gitbook'
|
||||
| 'gitbook-assistant'
|
||||
| 'cursor'
|
||||
| 'vscode'
|
||||
| 'mcp'
|
||||
| 'chatgpt'
|
||||
| 'claude';
|
||||
|
||||
export type IconName = BasicIconName | CustomIconName;
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @gitbook/react-contentkit
|
||||
|
||||
## 0.7.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [262afa3]
|
||||
- @gitbook/icons@0.3.1
|
||||
|
||||
## 0.7.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@gitbook/react-contentkit",
|
||||
"version": "0.7.4",
|
||||
"version": "0.7.5",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @gitbook/react-openapi
|
||||
|
||||
## 1.4.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6da3655]
|
||||
- @gitbook/expr@1.2.0
|
||||
|
||||
## 1.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3548fa6]
|
||||
- @gitbook/expr@1.1.1
|
||||
|
||||
## 1.4.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 24f601d: Small optim in resolveTryItPrefillForOperation
|
||||
- Updated dependencies [e1ff17e]
|
||||
- Updated dependencies [8ff1e3b]
|
||||
- @gitbook/expr@1.1.0
|
||||
|
||||
## 1.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "1.4.0",
|
||||
"version": "1.4.3",
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@gitbook/expr": "workspace:*",
|
||||
|
||||
@@ -27,12 +27,16 @@ export function resolveTryItPrefillForOperation(args: {
|
||||
prefillInputContext,
|
||||
} = args;
|
||||
|
||||
// Fixed ExpressionRuntime and resolveTryItPrefillExpression function
|
||||
if (!prefillInputContext) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const runtime = new ExpressionRuntime();
|
||||
const resolveTryItPrefillExpression = (expr: string) => {
|
||||
if (!prefillInputContext) return undefined;
|
||||
const parts = parseTemplate(expr);
|
||||
if (!parts.length) return undefined;
|
||||
if (!parts.length) {
|
||||
return undefined;
|
||||
}
|
||||
return runtime.evaluateTemplate(expr, prefillInputContext);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user