API to let integrations define custom tools for Docs Assistant (#3568)

This commit is contained in:
Samy Pessé
2025-08-17 10:03:54 +02:00
committed by GitHub
parent 1420180220
commit cbc71a56b6
18 changed files with 497 additions and 148 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@gitbook/browser-types": minor
---
First version of the public package for typing script integrations.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": minor
---
Allow integrations to provide tools to the Docs Assistant
+16 -2
View File
@@ -10,6 +10,17 @@
"vercel": "^39.3.0",
},
},
"packages/browser-types": {
"name": "@gitbook/browser-types",
"version": "0.3.1",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/icons": "workspace:",
},
"devDependencies": {
"typescript": "^5.5.3",
},
},
"packages/cache-tags": {
"name": "@gitbook/cache-tags",
"version": "0.3.1",
@@ -51,6 +62,7 @@
"version": "0.15.0",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/browser-types": "workspace:*",
"@gitbook/cache-tags": "workspace:*",
"@gitbook/colors": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*",
@@ -246,7 +258,7 @@
"react-dom": "^19.0.0",
},
"catalog": {
"@gitbook/api": "^0.134.0",
"@gitbook/api": "^0.136.0",
},
"packages": {
"@ai-sdk/provider": ["@ai-sdk/provider@1.1.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="],
@@ -611,7 +623,9 @@
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="],
"@gitbook/api": ["@gitbook/api@0.134.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-WMdLsA0ZOTbOyoloevPs0qa/VR2xmfp+YB6T/a2o8fkFUv5fMXxDVfCAcIxB2q9NCmkriMSCohWKmxLfz44s6w=="],
"@gitbook/api": ["@gitbook/api@0.136.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-IxNqmXE6yUEUq0IzbenN8S/PcMfgxr4+akY8xh6V5ShI/+U37ixujJHZp2zJq6NdZOdBQoR3UQmhPnvtWrkF7g=="],
"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],
"@gitbook/cache-tags": ["@gitbook/cache-tags@workspace:packages/cache-tags"],
+1 -1
View File
@@ -34,7 +34,7 @@
"workspaces": {
"packages": ["packages/*"],
"catalog": {
"@gitbook/api": "^0.134.0"
"@gitbook/api": "^0.136.0"
}
},
"patchedDependencies": {
+1
View File
@@ -0,0 +1 @@
dist/
+3
View File
@@ -0,0 +1,3 @@
# `@gitbook/browser-types`
Typescript types for the global variables available in a GitBook website. These types can be used by integrations embedding scripts.
+24
View File
@@ -0,0 +1,24 @@
{
"name": "@gitbook/browser-types",
"description": "Typescript types for the global variables available in a GitBook website. These types can be used by integrations embedding scripts.",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"version": "0.0.0",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/icons": "workspace:"
},
"devDependencies": {
"typescript": "^5.5.3"
},
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit"
},
"files": ["dist", "README.md", "CHANGELOG.md"]
}
+54
View File
@@ -0,0 +1,54 @@
import type { AIToolCallResult, AIToolDefinition } from '@gitbook/api';
import type { IconName } from '@gitbook/icons';
export type GitBookIntegrationEvent = 'load' | 'unload';
export type GitBookIntegrationEventCallback = (...args: any[]) => void;
export type GitBookIntegrationTool = AIToolDefinition & {
/**
* Confirmation action to be displayed to the user before executing the tool.
*/
confirmation?: {
icon?: IconName;
label: string;
};
/**
* Callback when the tool is executed.
* The input is provided by the AI assistant following the input schema of the tool.
*/
execute: (input: object) => Promise<Pick<AIToolCallResult, 'output' | 'summary'>>;
};
export type GitBookGlobal = {
/**
* Register an event listener.
*/
addEventListener: (
type: GitBookIntegrationEvent,
func: GitBookIntegrationEventCallback
) => void;
/**
* Remove an event listener.
*/
removeEventListener: (
type: GitBookIntegrationEvent,
func: GitBookIntegrationEventCallback
) => void;
/**
* Register a custom tool to be exposed to the AI assistant.
*/
registerTool: (tool: GitBookIntegrationTool) => void;
};
declare global {
interface Window {
/**
* Global `window.GitBook` object accessible by integrations.
*/
GitBook?: GitBookGlobal;
}
}
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": false,
"declaration": true,
"outDir": "dist",
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"types": [
"bun-types" // add Bun global
]
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}
+1
View File
@@ -6,6 +6,7 @@
"@gitbook/api": "catalog:",
"@gitbook/cache-tags": "workspace:*",
"@gitbook/colors": "workspace:*",
"@gitbook/browser-types": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*",
"@gitbook/fonts": "workspace:*",
"@gitbook/icons": "workspace:*",
@@ -10,6 +10,7 @@ import type {
AIToolCallGetPageContent,
AIToolCallGetPages,
AIToolCallMCP,
AIToolCallOther,
AIToolCallSearch,
ContentRef,
} from '@gitbook/api';
@@ -58,6 +59,8 @@ function getDescriptionForToolCall(toolCall: AIToolCall, context: GitBookSiteCon
return <DescriptionForGetPagesToolCall toolCall={toolCall} context={context} />;
case 'mcp':
return <DescriptionForMCPToolCall toolCall={toolCall} context={context} />;
case 'other':
return <DescriptionForOtherToolCall toolCall={toolCall} context={context} />;
default:
return <>{toolCall.tool}</>;
}
@@ -112,6 +115,15 @@ function DescriptionForMCPToolCall(props: {
);
}
function DescriptionForOtherToolCall(props: {
toolCall: AIToolCallOther;
context: GitBookSiteContext;
}) {
const { toolCall } = props;
return <p>{toolCall.summary.text}</p>;
}
async function DescriptionForSearchToolCall(props: {
toolCall: AIToolCallSearch;
context: GitBookSiteContext;
@@ -247,6 +259,8 @@ function getIconForToolCall(toolCall: AIToolCall): IconName {
return 'magnifying-glass';
case 'getPages':
return 'files';
case 'other':
return (toolCall.summary.icon as IconName) ?? 'hammer';
default:
return 'hammer';
}
@@ -2,7 +2,13 @@
import { getSiteURLDataFromMiddleware } from '@/lib/middleware';
import { getServerActionBaseContext } from '@/lib/server-actions';
import { traceErrorOnly } from '@/lib/tracing';
import { type AIMessageContext, AIMessageRole, AIModel } from '@gitbook/api';
import {
type AIMessageContext,
AIMessageRole,
AIModel,
type AIToolCallResult,
type AIToolDefinition,
} from '@gitbook/api';
import { streamRenderAIMessage } from './api';
import type { RenderAIMessageOptions } from './types';
@@ -13,11 +19,15 @@ export async function* streamAIChatResponse({
message,
messageContext,
previousResponseId,
toolCall,
tools,
options,
}: {
message: string;
message?: string;
messageContext: AIMessageContext;
previousResponseId?: string;
toolCall?: AIToolCallResult;
tools?: AIToolDefinition[];
options?: RenderAIMessageOptions;
}) {
const { stream } = await traceErrorOnly('AI.streamAIChatResponse', async () => {
@@ -29,17 +39,19 @@ export async function* streamAIChatResponse({
siteURLData.organization,
siteURLData.site,
{
mode: 'assistant',
input: [
{
role: AIMessageRole.User,
content: message,
context: messageContext,
},
],
output: { type: 'document' },
input: message
? [
{
role: AIMessageRole.User,
content: message,
context: messageContext,
},
]
: [],
model: AIModel.ReasoningLow,
previousResponseId,
toolCall,
tools,
}
);
+242 -100
View File
@@ -2,9 +2,15 @@
import * as zustand from 'zustand';
import { AIMessageRole } from '@gitbook/api';
import {
AIMessageRole,
type AIStreamResponseToolCallPending,
type AIToolCallResult,
} from '@gitbook/api';
import type { IconName } from '@gitbook/icons';
import * as React from 'react';
import { useTrackEvent } from '../Insights';
import { integrationsAssistantTools } from '../Integrations';
import { useSearch } from '../Search';
import { streamAIChatResponse } from './server-actions';
import { useAIMessageContextRef } from './useAIMessageContext';
@@ -15,6 +21,21 @@ export type AIChatMessage = {
query?: string;
};
export type AIChatPendingTool = {
icon?: IconName;
label: string;
/**
* Confirm the tool call by calling this function.
*/
confirm: () => Promise<void>;
/**
* Tool call result to cancel it.
*/
cancelToolCall: AIToolCallResult;
};
export type AIChatState = {
/**
* If true, the chat is open.
@@ -46,6 +67,11 @@ export type AIChatState = {
*/
followUpSuggestions: string[];
/**
* Tools that are pending confirmation to be executed.
*/
pendingTools: AIChatPendingTool[];
/**
* If true, the session is in progress.
*/
@@ -71,22 +97,17 @@ export type AIChatController = {
};
// Global state store for AI chat
const globalState = zustand.create<{
state: AIChatState;
setState: (fn: (state: AIChatState) => Partial<AIChatState>) => void;
}>((set) => {
const globalState = zustand.create<AIChatState>(() => {
return {
state: {
opened: false,
responseId: null,
messages: [],
query: null,
followUpSuggestions: [],
loading: false,
error: false,
initialQuery: null,
},
setState: (fn) => set((state) => ({ state: { ...state.state, ...fn(state.state) } })),
opened: false,
responseId: null,
messages: [],
query: null,
followUpSuggestions: [],
pendingTools: [],
loading: false,
error: false,
initialQuery: null,
};
});
@@ -94,7 +115,7 @@ const globalState = zustand.create<{
* Get the current state of the AI chat.
*/
export function useAIChatState(): AIChatState {
const state = zustand.useStore(globalState, (state) => state.state);
const state = zustand.useStore(globalState);
return state;
}
@@ -104,14 +125,13 @@ export function useAIChatState(): AIChatState {
*/
export function useAIChatController(): AIChatController {
const messageContextRef = useAIMessageContextRef();
const setState = zustand.useStore(globalState, (state) => state.setState);
const trackEvent = useTrackEvent();
const [searchState, setSearchState] = useSearch(true);
// Open AI chat and sync with search state
const onOpen = React.useCallback(() => {
const { initialQuery } = globalState.getState().state;
setState((state) => ({ ...state, opened: true }));
const { initialQuery } = globalState.getState();
globalState.setState((state) => ({ ...state, opened: true }));
// Update search state to show ask mode with first message or current ask value
setSearchState((prev) => ({
@@ -120,11 +140,11 @@ export function useAIChatController(): AIChatController {
global: prev?.global ?? false,
open: false, // Close search popover when opening chat
}));
}, [setState, setSearchState]);
}, [setSearchState]);
// Close AI chat and clear ask parameter
const onClose = React.useCallback(() => {
setState((state) => ({ ...state, opened: false }));
globalState.setState((state) => ({ ...state, opened: false }));
// Clear ask parameter but keep other search state
setSearchState((prev) => ({
@@ -133,12 +153,196 @@ export function useAIChatController(): AIChatController {
global: prev?.global ?? false,
open: false,
}));
}, [setState, setSearchState]);
}, [setSearchState]);
// Stream a message with the AI backend
const streamResponse = React.useCallback(
async (input: {
/** Text message to send to the AI backend */
message?: string;
/** Tool call to send to the AI backend */
toolCall?: AIToolCallResult;
}) => {
globalState.setState((state) => {
return {
...state,
followUpSuggestions: [],
pendingTools: [],
loading: true,
error: false,
messages: [
...state.messages,
{
role: AIMessageRole.Assistant,
content: null, // Placeholder for streaming response
},
],
};
});
// Execute a tool call
const executeToolCall = async (event: AIStreamResponseToolCallPending) => {
const integrationTools = integrationsAssistantTools.getState().tools;
const toolDef = integrationTools.find((tool) => tool.name === event.toolCall.tool);
if (!toolDef) {
throw new Error(`Tool ${event.toolCall.tool} not found`);
}
try {
const result = await toolDef.execute(event.toolCall.input);
streamResponse({
toolCall: {
tool: event.toolCall.tool,
toolCallId: event.toolCallId,
output: result.output,
summary: result.summary,
},
});
} catch (error) {
streamResponse({
toolCall: {
tool: event.toolCall.tool,
toolCallId: event.toolCallId,
output: {
error: error instanceof Error ? error.message : 'Unknown error',
},
summary: {
icon: 'bomb',
text: 'An error occurred while executing the tool',
},
},
});
}
};
let toolToExecute: AIStreamResponseToolCallPending | null = null;
try {
const integrationTools = integrationsAssistantTools.getState().tools;
const stream = await streamAIChatResponse({
message: input.message,
toolCall: input.toolCall,
messageContext: messageContextRef.current,
previousResponseId: globalState.getState().responseId ?? undefined,
tools: integrationTools.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
});
// Process streaming response
for await (const data of stream) {
if (!data) continue;
if (input.message && globalState.getState().query !== input.message) {
// Chat was cleared, stop processing the stream
break;
}
const event = data.event;
switch (event.type) {
case 'response_finish': {
globalState.setState((state) => ({
...state,
responseId: event.responseId,
// Mark as not loading when the response is finished
// Even if the stream might continue as we receive 'response_followup_suggestion'
loading: false,
error: false,
}));
break;
}
case 'response_followup_suggestion': {
globalState.setState((state) => ({
...state,
followUpSuggestions: [
...state.followUpSuggestions,
...event.suggestions,
],
}));
break;
}
case 'response_tool_call_pending': {
const toolDef = integrationTools.find(
(tool) => tool.name === event.toolCall.tool
);
if (!toolDef) {
throw new Error(`Tool ${event.toolCall.tool} not found`);
}
const confirmation = toolDef.confirmation;
if (confirmation) {
globalState.setState((state) => ({
...state,
pendingTools: [
...state.pendingTools,
{
icon: confirmation.icon,
label: confirmation.label,
cancelToolCall: {
tool: event.toolCall.tool,
toolCallId: event.toolCallId,
output: {
cancelled: 'User did not confirm the tool call',
},
summary: {
icon: 'forward',
text: `Skipped confirmation of "${confirmation.label}"`,
},
},
confirm: async () => {
await executeToolCall(event);
},
},
],
}));
} else {
toolToExecute = event;
}
break;
}
}
// Update the assistant message with streamed content
globalState.setState((state) => ({
...state,
messages: [
...state.messages.slice(0, -1),
{
role: AIMessageRole.Assistant,
content: data.content,
},
],
}));
}
// Execute the tool call if it doesn't require confirmation
if (toolToExecute) {
await executeToolCall(toolToExecute);
}
globalState.setState((state) => ({
...state,
loading: false,
error: false,
}));
} catch {
globalState.setState((state) => ({
...state,
loading: false,
error: true,
}));
}
},
[messageContextRef.current]
);
// Post a message to the AI chat
const onPostMessage = React.useCallback(
async (input: { message: string }) => {
const { query, messages } = globalState.getState().state;
const { query, messages, pendingTools } = globalState.getState();
// For first message, update the ask parameter in URL
if (messages.length === 0) {
@@ -158,7 +362,7 @@ export function useAIChatController(): AIChatController {
trackEvent({ type: 'ask_question', query: input.message });
// Add user message and placeholder for AI response
setState((state) => {
globalState.setState((state) => {
return {
...state,
messages: [
@@ -168,95 +372,34 @@ export function useAIChatController(): AIChatController {
content: input.message,
query: input.message,
},
{
role: AIMessageRole.Assistant,
content: null, // Placeholder for streaming response
},
],
query: input.message,
responseId: null,
followUpSuggestions: [],
loading: true,
error: false,
};
});
try {
const stream = await streamAIChatResponse({
message: input.message,
messageContext: messageContextRef.current,
previousResponseId: globalState.getState().state.responseId ?? undefined,
});
// Process streaming response
for await (const data of stream) {
if (!data) continue;
if (globalState.getState().state.query !== input.message) break; // Chat was cleared, stop processing the stream
const event = data.event;
switch (event.type) {
case 'response_finish': {
setState((state) => ({
...state,
responseId: event.responseId,
// Mark as not loading when the response is finished
// Even if the stream might continue as we receive 'response_followup_suggestion'
loading: false,
error: false,
}));
break;
}
case 'response_followup_suggestion': {
setState((state) => ({
...state,
followUpSuggestions: [
...state.followUpSuggestions,
...event.suggestions,
],
}));
break;
}
}
// Update the assistant message with streamed content
setState((state) => ({
...state,
messages: [
...state.messages.slice(0, -1),
{
role: AIMessageRole.Assistant,
content: data.content,
},
],
}));
}
setState((state) => ({
...state,
loading: false,
error: false,
}));
} catch {
setState((state) => ({
...state,
loading: false,
error: true,
}));
}
const pendingTool = pendingTools[0];
streamResponse({
message: input.message,
// If we had a pending tool call, we need to send it as being cancelled
// otherwise the AI will fail to process the message
...(pendingTool ? { toolCall: pendingTool.cancelToolCall } : {}),
});
},
[messageContextRef.current, setState, setSearchState, trackEvent]
[setSearchState, trackEvent, streamResponse]
);
// Clear the conversation and reset ask parameter
const onClear = React.useCallback(() => {
setState((state) => ({
globalState.setState((state) => ({
opened: state.opened,
loading: false,
messages: [],
query: null,
followUpSuggestions: [],
pendingTools: [],
responseId: null,
error: false,
initialQuery: null,
@@ -269,7 +412,7 @@ export function useAIChatController(): AIChatController {
global: prev?.global ?? false,
open: false,
}));
}, [setState, setSearchState]);
}, [setSearchState]);
// Auto-trigger AI chat when ?ask= parameter appears in URL (only once)
React.useEffect(() => {
@@ -286,7 +429,7 @@ export function useAIChatController(): AIChatController {
// Auto-post the message if ask has content
if (searchState?.ask?.trim()) {
const trimmedAsk = searchState.ask.trim();
const { loading, initialQuery } = globalState.getState().state;
const { loading, initialQuery } = globalState.getState();
// Don't trigger if we're already posting a message
if (loading) return;
@@ -298,7 +441,7 @@ export function useAIChatController(): AIChatController {
if (!messageContextRef.current?.location) return;
// Mark this ask value as processed
setState((state) => ({ ...state, initialQuery: trimmedAsk }));
globalState.setState((state) => ({ ...state, initialQuery: trimmedAsk }));
onPostMessage({ message: trimmedAsk });
}
}, [
@@ -307,7 +450,6 @@ export function useAIChatController(): AIChatController {
searchState?.open,
messageContextRef,
onOpen,
setState,
onPostMessage,
]);
@@ -2,6 +2,7 @@ import { tcls } from '@/lib/tailwind';
import { AIMessageRole } from '@gitbook/api';
import type React from 'react';
import type { AIChatController, AIChatState } from '../AI';
import { AIChatToolConfirmations } from './AIChatToolConfirmations';
import { AIResponseFeedback } from './AIResponseFeedback';
import { AIChatFollowupSuggestions } from './AiChatFollowupSuggestions';
@@ -60,13 +61,18 @@ export function AIChatMessages(props: {
{isLastMessage ? (
<>
{!chat.loading && !chat.error && chat.query && chat.responseId && (
{!chat.loading &&
!chat.error &&
chat.query &&
chat.responseId &&
chat.pendingTools.length === 0 ? (
<AIResponseFeedback
responseId={chat.responseId}
query={chat.query}
className="-ml-1 -mt-4"
/>
)}
) : null}
<AIChatToolConfirmations chat={chat} />
<AIChatFollowupSuggestions
chat={chat}
chatController={chatController}
@@ -0,0 +1,36 @@
import type { AIChatState } from '../AI';
import { Button } from '../primitives';
/**
* Display buttons to confirm tool calls.
*/
export function AIChatToolConfirmations(props: {
chat: AIChatState;
}) {
const { chat } = props;
if (chat.pendingTools.length === 0) {
return null;
}
return (
<div className="flex w-full flex-wrap justify-end gap-2">
{chat.pendingTools.map((tool, index) => (
<Button
key={index}
onClick={() => {
tool.confirm();
}}
label={tool.label}
className="whitespace-normal! max-w-full animate-[present_500ms_both] text-left ring-1 ring-tint-subtle"
size="medium"
variant="primary"
icon={tool.icon}
style={{
animationDelay: `${250 + Math.min(index * 50, 150)}ms`,
}}
/>
))}
</div>
);
}
@@ -1,23 +1,49 @@
'use client';
import * as React from 'react';
import * as zustand from 'zustand';
import type {
GitBookGlobal,
GitBookIntegrationEvent,
GitBookIntegrationEventCallback,
GitBookIntegrationTool,
} from '@gitbook/browser-types';
const events = new Map<GitBookIntegrationEvent, GitBookIntegrationEventCallback[]>();
export const integrationsAssistantTools = zustand.createStore<{
/**
* Tools exposed to the assistant by integrations
*/
tools: GitBookIntegrationTool[];
}>(() => {
return {
tools: [],
};
});
if (typeof window !== 'undefined') {
window.GitBook = {
events: new Map(),
const gitbookGlobal: GitBookGlobal = {
addEventListener: (event, callback) => {
const handlers = window.GitBook?.events.get(event) ?? [];
const handlers = events.get(event) ?? [];
handlers.push(callback);
window.GitBook?.events.set(event, handlers);
events.set(event, handlers);
},
removeEventListener: (event, callback) => {
const handlers = window.GitBook?.events.get(event) ?? [];
const handlers = events.get(event) ?? [];
const index = handlers.indexOf(callback);
if (index !== -1) {
handlers.splice(index, 1);
}
},
registerTool: (tool) => {
integrationsAssistantTools.setState((state) => ({
tools: [...state.tools, tool],
}));
},
};
window.GitBook = gitbookGlobal;
}
/**
@@ -34,6 +60,5 @@ export function LoadIntegrations() {
* Client function to dispatch a GitBook event.
*/
function dispatchGitBookIntegrationEvent(type: GitBookIntegrationEvent, ...args: any[]) {
const handlers = window.GitBook?.events.get(type) || [];
handlers.forEach((handler) => handler(...args));
events.get(type)?.forEach((handler) => handler(...args));
}
+8 -1
View File
@@ -25,7 +25,14 @@
"bun-types" // add Bun global
]
},
"include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx", "types/**/*.d.ts"],
"include": [
"next-env.d.ts",
".next/types/**/*.ts",
"**/*.ts",
"**/*.tsx",
"types/**/*.d.ts",
"@gitbook/browser-types"
],
"exclude": [
"node_modules",
"packages/openapi-parser",
-24
View File
@@ -1,24 +0,0 @@
declare global {
type GitBookIntegrationEvent = 'load' | 'unload';
type GitBookIntegrationEventCallback = (...args: any[]) => void;
interface Window {
/**
* Global `window.GitBook` object accessible by integrations.
*/
GitBook?: {
events: Map<GitBookIntegrationEvent, GitBookIntegrationEventCallback[]>;
addEventListener: (
type: GitBookIntegrationEvent,
func: GitBookIntegrationEventCallback
) => void;
removeEventListener: (
type: GitBookIntegrationEvent,
func: GitBookIntegrationEventCallback
) => void;
};
}
}
export {};