mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-17 08:05:19 +00:00
Embeddable GitBook API (#3593)
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@gitbook/embed": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Improve API to control the GitBook embed
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Support customization of buttons and tools through iframe API
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
"**/.vercel/**/*",
|
"**/.vercel/**/*",
|
||||||
"**/.cache/**/*",
|
"**/.cache/**/*",
|
||||||
"**/.wrangler/**/*",
|
"**/.wrangler/**/*",
|
||||||
|
"packages/embed/standalone/**/*",
|
||||||
"packages/openapi-parser/src/fixtures/**/*",
|
"packages/openapi-parser/src/fixtures/**/*",
|
||||||
"packages/emoji-codepoints/index.ts",
|
"packages/emoji-codepoints/index.ts",
|
||||||
"packages/icons/src/data/*.json",
|
"packages/icons/src/data/*.json",
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { createChannel } from 'bidc';
|
import { createChannel } from 'bidc';
|
||||||
import type {
|
import type {
|
||||||
FrameToParentMessage,
|
FrameToParentMessage,
|
||||||
GitBookPlaceholderSettings,
|
GitBookEmbeddableConfiguration,
|
||||||
GitBookToolDefinition,
|
|
||||||
ParentToFrameMessage,
|
ParentToFrameMessage,
|
||||||
} from './protocol';
|
} from './protocol';
|
||||||
|
|
||||||
@@ -22,11 +21,6 @@ export type GitBookFrameClient = {
|
|||||||
*/
|
*/
|
||||||
postUserMessage: (message: string) => void;
|
postUserMessage: (message: string) => void;
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a custom tool.
|
|
||||||
*/
|
|
||||||
registerTool: (tool: GitBookToolDefinition) => void;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear the chat.
|
* Clear the chat.
|
||||||
*/
|
*/
|
||||||
@@ -35,7 +29,7 @@ export type GitBookFrameClient = {
|
|||||||
/**
|
/**
|
||||||
* Set the placeholder settings.
|
* Set the placeholder settings.
|
||||||
*/
|
*/
|
||||||
setPlaceholder: (placeholder: GitBookPlaceholderSettings) => void;
|
configure: (settings: Partial<GitBookEmbeddableConfiguration>) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register an event listener.
|
* Register an event listener.
|
||||||
@@ -53,6 +47,7 @@ export function createGitBookFrame(iframe: HTMLIFrameElement): GitBookFrameClien
|
|||||||
const channel = createChannel(iframe.contentWindow);
|
const channel = createChannel(iframe.contentWindow);
|
||||||
|
|
||||||
channel.receive((message: FrameToParentMessage) => {
|
channel.receive((message: FrameToParentMessage) => {
|
||||||
|
console.log('[gitbook:embed] received message', message);
|
||||||
if (message.type === 'close') {
|
if (message.type === 'close') {
|
||||||
const listeners = events.get('close') || [];
|
const listeners = events.get('close') || [];
|
||||||
if (listeners) {
|
if (listeners) {
|
||||||
@@ -62,11 +57,19 @@ export function createGitBookFrame(iframe: HTMLIFrameElement): GitBookFrameClien
|
|||||||
});
|
});
|
||||||
|
|
||||||
const sendToFrame = (message: ParentToFrameMessage) => {
|
const sendToFrame = (message: ParentToFrameMessage) => {
|
||||||
|
console.log('[gitbook:embed] send message', message);
|
||||||
channel.send(message);
|
channel.send(message);
|
||||||
};
|
};
|
||||||
|
|
||||||
const events = new Map<string, Array<(...args: any[]) => void>>();
|
const events = new Map<string, Array<(...args: any[]) => void>>();
|
||||||
|
|
||||||
|
const configuration: GitBookEmbeddableConfiguration = {
|
||||||
|
buttons: [],
|
||||||
|
welcomeMessage: '',
|
||||||
|
suggestions: [],
|
||||||
|
tools: [],
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
navigateToPage: (pagePath) => {
|
navigateToPage: (pagePath) => {
|
||||||
sendToFrame({ type: 'navigateToPage', pagePath });
|
sendToFrame({ type: 'navigateToPage', pagePath });
|
||||||
@@ -75,9 +78,11 @@ export function createGitBookFrame(iframe: HTMLIFrameElement): GitBookFrameClien
|
|||||||
sendToFrame({ type: 'navigateToAssistant' });
|
sendToFrame({ type: 'navigateToAssistant' });
|
||||||
},
|
},
|
||||||
postUserMessage: (message) => sendToFrame({ type: 'postUserMessage', message }),
|
postUserMessage: (message) => sendToFrame({ type: 'postUserMessage', message }),
|
||||||
registerTool: (tool) => sendToFrame({ type: 'registerTool', tool }),
|
configure: (settings) => {
|
||||||
|
Object.assign(configuration, settings);
|
||||||
|
sendToFrame({ type: 'configure', settings: configuration });
|
||||||
|
},
|
||||||
clearChat: () => sendToFrame({ type: 'clearChat' }),
|
clearChat: () => sendToFrame({ type: 'clearChat' }),
|
||||||
setPlaceholder: (settings) => sendToFrame({ type: 'setPlaceholder', settings }),
|
|
||||||
on: (event, listener) => {
|
on: (event, listener) => {
|
||||||
const listeners = events.get(event) || [];
|
const listeners = events.get(event) || [];
|
||||||
listeners.push(listener);
|
listeners.push(listener);
|
||||||
|
|||||||
@@ -21,18 +21,42 @@ export type GitBookToolDefinition = AIToolDefinition & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Placeholder settings.
|
* Custom button definition to be passed to the embeddable GitBook.
|
||||||
*/
|
*/
|
||||||
export type GitBookPlaceholderSettings = {
|
export type GitBookEmbeddableButtonDefinition = {
|
||||||
/**
|
/**
|
||||||
* Welcome message to be displayed in the placeholder.
|
* Icon to be displayed in the button.
|
||||||
*/
|
*/
|
||||||
welcomeMessage: string;
|
icon: IconName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Suggestions to be displayed in the placeholder.
|
* Label to be displayed in the button.
|
||||||
*/
|
*/
|
||||||
|
label: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback when the button is clicked.
|
||||||
|
*/
|
||||||
|
onClick: () => void | Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overall configuration for the layout of the embeddable GitBook.
|
||||||
|
*/
|
||||||
|
export type GitBookEmbeddableConfiguration = {
|
||||||
|
/**
|
||||||
|
* Buttons to be displayed in the header of the embeddable GitBook.
|
||||||
|
*/
|
||||||
|
buttons: GitBookEmbeddableButtonDefinition[];
|
||||||
|
|
||||||
|
/** Message to be displayed in the welcome page. */
|
||||||
|
welcomeMessage: string;
|
||||||
|
|
||||||
|
/** Suggestions of questions to be displayed in the welcome page. */
|
||||||
suggestions: string[];
|
suggestions: string[];
|
||||||
|
|
||||||
|
/** Tools to be provided to the assistant. */
|
||||||
|
tools: GitBookToolDefinition[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,16 +67,12 @@ export type ParentToFrameMessage =
|
|||||||
type: 'postUserMessage';
|
type: 'postUserMessage';
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
type: 'registerTool';
|
|
||||||
tool: GitBookToolDefinition;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
type: 'clearChat';
|
type: 'clearChat';
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'setPlaceholder';
|
type: 'configure';
|
||||||
settings: GitBookPlaceholderSettings;
|
settings: GitBookEmbeddableConfiguration;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'navigateToPage';
|
type: 'navigateToPage';
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import type { GetFrameURLOptions, GitBookFrameClient } from '../client';
|
|
||||||
import { useGitBook } from './GitBookProvider';
|
|
||||||
|
|
||||||
export type GitBookAssistantFrameProps = {
|
|
||||||
title?: string;
|
|
||||||
className?: string;
|
|
||||||
} & GetFrameURLOptions;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render a frame with the GitBook Assistant in it.
|
|
||||||
*/
|
|
||||||
export function GitBookAssistantFrame(props: GitBookAssistantFrameProps) {
|
|
||||||
const { title, className, ...frameOptions } = props;
|
|
||||||
|
|
||||||
const frameRef = React.useRef<HTMLIFrameElement>(null);
|
|
||||||
const gitbookFrameRef = React.useRef<GitBookFrameClient | null>(null);
|
|
||||||
const gitbook = useGitBook();
|
|
||||||
const frameURL = gitbook.getFrameURL(frameOptions);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (frameRef.current) {
|
|
||||||
gitbookFrameRef.current = gitbook.createFrame(frameRef.current);
|
|
||||||
}
|
|
||||||
}, [gitbook]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<iframe
|
|
||||||
title={title ?? 'GitBook Assistant'}
|
|
||||||
ref={frameRef}
|
|
||||||
src={frameURL}
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type {
|
||||||
|
GetFrameURLOptions,
|
||||||
|
GitBookEmbeddableConfiguration,
|
||||||
|
GitBookFrameClient,
|
||||||
|
} from '../client';
|
||||||
|
import { useGitBook } from './GitBookProvider';
|
||||||
|
|
||||||
|
export type GitBookFrameProps = {
|
||||||
|
className?: string;
|
||||||
|
} & GetFrameURLOptions &
|
||||||
|
GitBookEmbeddableConfiguration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a frame with the GitBook Assistant in it.
|
||||||
|
*/
|
||||||
|
export function GitBookFrame(props: GitBookFrameProps) {
|
||||||
|
const { className, visitor, buttons, welcomeMessage, suggestions, tools } = props;
|
||||||
|
|
||||||
|
const frameRef = React.useRef<HTMLIFrameElement>(null);
|
||||||
|
const gitbook = useGitBook();
|
||||||
|
const [gitbookFrame, setGitbookFrame] = React.useState<GitBookFrameClient | null>(null);
|
||||||
|
|
||||||
|
const frameURL = React.useMemo(() => gitbook.getFrameURL({ visitor }), [gitbook, visitor]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (frameRef.current) {
|
||||||
|
setGitbookFrame(gitbook.createFrame(frameRef.current));
|
||||||
|
}
|
||||||
|
}, [gitbook]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
gitbookFrame?.configure({
|
||||||
|
buttons,
|
||||||
|
welcomeMessage,
|
||||||
|
suggestions,
|
||||||
|
tools,
|
||||||
|
});
|
||||||
|
}, [gitbookFrame, buttons, welcomeMessage, suggestions, tools]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
title="GitBook"
|
||||||
|
ref={frameRef}
|
||||||
|
src={frameURL}
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
export * from './GitBookProvider';
|
export * from './GitBookProvider';
|
||||||
|
export * from './GitBookFrame';
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ import {
|
|||||||
type CreateGitBookOptions,
|
type CreateGitBookOptions,
|
||||||
type GetFrameURLOptions,
|
type GetFrameURLOptions,
|
||||||
type GitBookClient,
|
type GitBookClient,
|
||||||
|
type GitBookEmbeddableConfiguration,
|
||||||
type GitBookFrameClient,
|
type GitBookFrameClient,
|
||||||
type GitBookPlaceholderSettings,
|
|
||||||
type GitBookToolDefinition,
|
|
||||||
createGitBook,
|
createGitBook,
|
||||||
} from '../client';
|
} from '../client';
|
||||||
|
|
||||||
@@ -29,12 +28,10 @@ type StandaloneCalls =
|
|||||||
| ['toggle']
|
| ['toggle']
|
||||||
// Post a user message
|
// Post a user message
|
||||||
| ['postUserMessage', string]
|
| ['postUserMessage', string]
|
||||||
// Register a tool
|
|
||||||
| ['registerTool', GitBookToolDefinition]
|
|
||||||
// Clear the chat
|
// Clear the chat
|
||||||
| ['clearChat']
|
| ['clearChat']
|
||||||
// Configure the placeholder
|
// Configure the embed
|
||||||
| ['setPlaceholder', GitBookPlaceholderSettings]
|
| ['configure', Partial<GitBookEmbeddableConfiguration>]
|
||||||
// Navigate to a page
|
// Navigate to a page
|
||||||
| ['navigateToPage', string]
|
| ['navigateToPage', string]
|
||||||
// Navigate to the assistant
|
// Navigate to the assistant
|
||||||
@@ -65,6 +62,12 @@ let widgetIframe: HTMLIFrameElement | undefined;
|
|||||||
let _client: GitBookClient | undefined;
|
let _client: GitBookClient | undefined;
|
||||||
let _frame: GitBookFrameClient | undefined;
|
let _frame: GitBookFrameClient | undefined;
|
||||||
let frameOptions: GetFrameURLOptions | undefined;
|
let frameOptions: GetFrameURLOptions | undefined;
|
||||||
|
let frameConfiguration: GitBookEmbeddableConfiguration = {
|
||||||
|
buttons: [],
|
||||||
|
welcomeMessage: '',
|
||||||
|
suggestions: [],
|
||||||
|
tools: [],
|
||||||
|
};
|
||||||
|
|
||||||
function getClient() {
|
function getClient() {
|
||||||
if (!_client) {
|
if (!_client) {
|
||||||
@@ -132,15 +135,30 @@ const GitBook = (...args: StandaloneCalls) => {
|
|||||||
case 'postUserMessage':
|
case 'postUserMessage':
|
||||||
getIframe().frame.postUserMessage(args[1]);
|
getIframe().frame.postUserMessage(args[1]);
|
||||||
break;
|
break;
|
||||||
case 'registerTool':
|
case 'configure':
|
||||||
getIframe().frame.registerTool(args[1]);
|
frameConfiguration = {
|
||||||
|
...frameConfiguration,
|
||||||
|
...args[1],
|
||||||
|
};
|
||||||
|
getIframe().frame.configure({
|
||||||
|
...frameConfiguration,
|
||||||
|
buttons: [
|
||||||
|
...frameConfiguration.buttons,
|
||||||
|
|
||||||
|
// Always include a close button
|
||||||
|
{
|
||||||
|
icon: 'close',
|
||||||
|
label: 'Close',
|
||||||
|
onClick: () => {
|
||||||
|
GitBook('close');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case 'clearChat':
|
case 'clearChat':
|
||||||
getIframe().frame.clearChat();
|
getIframe().frame.clearChat();
|
||||||
break;
|
break;
|
||||||
case 'setPlaceholder':
|
|
||||||
getIframe().frame.setPlaceholder(args[1]);
|
|
||||||
break;
|
|
||||||
case 'navigateToPage':
|
case 'navigateToPage':
|
||||||
getIframe().frame.navigateToPage(args[1]);
|
getIframe().frame.navigateToPage(args[1]);
|
||||||
break;
|
break;
|
||||||
@@ -156,3 +174,5 @@ const precalls = (window.GitBook as GitBookStandalone | undefined)?.q ?? [];
|
|||||||
// @ts-expect-error - GitBook is not defined in the global scope
|
// @ts-expect-error - GitBook is not defined in the global scope
|
||||||
window.GitBook = GitBook;
|
window.GitBook = GitBook;
|
||||||
precalls.forEach((call) => GitBook(...call));
|
precalls.forEach((call) => GitBook(...call));
|
||||||
|
|
||||||
|
GitBook('configure', {});
|
||||||
|
|||||||
@@ -346,7 +346,8 @@ export function AIChatProvider(props: {
|
|||||||
loading: false,
|
loading: false,
|
||||||
error: false,
|
error: false,
|
||||||
}));
|
}));
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
console.error('Error streaming AI response', error);
|
||||||
globalState.setState((state) => ({
|
globalState.setState((state) => ({
|
||||||
...state,
|
...state,
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -359,6 +360,7 @@ export function AIChatProvider(props: {
|
|||||||
renderMessageOptions?.withLinkPreviews,
|
renderMessageOptions?.withLinkPreviews,
|
||||||
renderMessageOptions?.withToolCalls,
|
renderMessageOptions?.withToolCalls,
|
||||||
renderMessageOptions?.asEmbeddable,
|
renderMessageOptions?.asEmbeddable,
|
||||||
|
language,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,18 @@ import {
|
|||||||
useAIChatController,
|
useAIChatController,
|
||||||
useAIChatState,
|
useAIChatState,
|
||||||
} from '../AI';
|
} from '../AI';
|
||||||
import { EmbeddableFrame } from '../Embeddable/EmbeddableFrame';
|
import {
|
||||||
|
EmbeddableFrame,
|
||||||
|
EmbeddableFrameBody,
|
||||||
|
EmbeddableFrameButtons,
|
||||||
|
EmbeddableFrameHeader,
|
||||||
|
EmbeddableFrameHeaderMain,
|
||||||
|
EmbeddableFrameSubtitle,
|
||||||
|
EmbeddableFrameTitle,
|
||||||
|
} from '../Embeddable/EmbeddableFrame';
|
||||||
import { useNow } from '../hooks';
|
import { useNow } from '../hooks';
|
||||||
import { Button } from '../primitives';
|
import { Button } from '../primitives';
|
||||||
import { DropdownMenu, DropdownMenuItem } from '../primitives';
|
import { AIChatControlButton } from './AIChatControlButton';
|
||||||
import { AIChatIcon } from './AIChatIcon';
|
import { AIChatIcon } from './AIChatIcon';
|
||||||
import { AIChatInput } from './AIChatInput';
|
import { AIChatInput } from './AIChatInput';
|
||||||
import { AIChatMessages } from './AIChatMessages';
|
import { AIChatMessages } from './AIChatMessages';
|
||||||
@@ -66,45 +74,17 @@ export function AIChat(props: { trademark: boolean }) {
|
|||||||
data-testid="ai-chat"
|
data-testid="ai-chat"
|
||||||
className="ai-chat inset-y-0 right-0 z-40 mx-auto flex max-w-3xl animate-present scroll-mt-36 px-4 py-4 transition-all duration-300 sm:px-6 lg:fixed lg:w-80 lg:animate-enter-from-right lg:pr-4 lg:pl-0 xl:w-96"
|
className="ai-chat inset-y-0 right-0 z-40 mx-auto flex max-w-3xl animate-present scroll-mt-36 px-4 py-4 transition-all duration-300 sm:px-6 lg:fixed lg:w-80 lg:animate-enter-from-right lg:pr-4 lg:pl-0 xl:w-96"
|
||||||
>
|
>
|
||||||
<EmbeddableFrame
|
<EmbeddableFrame className="relative circular-corners:rounded-3xl rounded-corners:rounded-md depth-subtle:shadow-lg shadow-tint ring-1 ring-tint-subtle">
|
||||||
className="relative circular-corners:rounded-3xl rounded-corners:rounded-md depth-subtle:shadow-lg shadow-tint ring-1 ring-tint-subtle"
|
<EmbeddableFrameHeader>
|
||||||
icon={<AIChatDynamicIcon trademark={trademark} />}
|
<AIChatDynamicIcon trademark={trademark} />
|
||||||
title={getAIChatName(language, trademark)}
|
<EmbeddableFrameHeaderMain>
|
||||||
subtitle={
|
<EmbeddableFrameTitle>
|
||||||
chat.loading
|
{getAIChatName(language, trademark)}
|
||||||
? chat.messages[chat.messages.length - 1].content
|
</EmbeddableFrameTitle>
|
||||||
? tString(language, 'ai_chat_working')
|
<AIChatSubtitle chat={chat} />
|
||||||
: tString(language, 'ai_chat_thinking')
|
</EmbeddableFrameHeaderMain>
|
||||||
: ''
|
<EmbeddableFrameButtons>
|
||||||
}
|
<AIChatControlButton />
|
||||||
buttons={
|
|
||||||
<>
|
|
||||||
{chat.messages.length > 0 ? (
|
|
||||||
<DropdownMenu
|
|
||||||
button={
|
|
||||||
<Button
|
|
||||||
onClick={() => {}}
|
|
||||||
iconOnly
|
|
||||||
icon="ellipsis"
|
|
||||||
label={tString(language, 'actions')}
|
|
||||||
variant="blank"
|
|
||||||
size="default"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
chatController.clear();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon="broom-wide"
|
|
||||||
className="size-3 shrink-0 text-tint-subtle"
|
|
||||||
/>
|
|
||||||
{t(language, 'ai_chat_clear_conversation')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenu>
|
|
||||||
) : null}
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => chatController.close()}
|
onClick={() => chatController.close()}
|
||||||
iconOnly
|
iconOnly
|
||||||
@@ -113,10 +93,11 @@ export function AIChat(props: { trademark: boolean }) {
|
|||||||
variant="blank"
|
variant="blank"
|
||||||
size="default"
|
size="default"
|
||||||
/>
|
/>
|
||||||
</>
|
</EmbeddableFrameButtons>
|
||||||
}
|
</EmbeddableFrameHeader>
|
||||||
>
|
<EmbeddableFrameBody>
|
||||||
<AIChatBody chatController={chatController} chat={chat} trademark={trademark} />
|
<AIChatBody chatController={chatController} chat={chat} trademark={trademark} />
|
||||||
|
</EmbeddableFrameBody>
|
||||||
</EmbeddableFrame>
|
</EmbeddableFrame>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -139,7 +120,7 @@ export function AIChatDynamicIcon(props: {
|
|||||||
chat.error
|
chat.error
|
||||||
? 'error'
|
? 'error'
|
||||||
: chat.loading
|
: chat.loading
|
||||||
? chat.messages[chat.messages.length - 1].content
|
? chat.messages[chat.messages.length - 1]?.content
|
||||||
? 'working'
|
? 'working'
|
||||||
: 'thinking'
|
: 'thinking'
|
||||||
: chat.messages.length > 0
|
: chat.messages.length > 0
|
||||||
@@ -152,6 +133,24 @@ export function AIChatDynamicIcon(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subtitle of the AI chat window.
|
||||||
|
*/
|
||||||
|
export function AIChatSubtitle(props: {
|
||||||
|
chat: AIChatState;
|
||||||
|
}) {
|
||||||
|
const { chat } = props;
|
||||||
|
const language = useLanguage();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EmbeddableFrameSubtitle className={chat.loading ? 'h-3 opacity-11' : 'h-0 opacity-0'}>
|
||||||
|
{chat.messages[chat.messages.length - 1]?.content
|
||||||
|
? tString(language, 'ai_chat_working')
|
||||||
|
: tString(language, 'ai_chat_thinking')}
|
||||||
|
</EmbeddableFrameSubtitle>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Body of the AI chat window.
|
* Body of the AI chat window.
|
||||||
*/
|
*/
|
||||||
@@ -159,8 +158,10 @@ export function AIChatBody(props: {
|
|||||||
chatController: AIChatController;
|
chatController: AIChatController;
|
||||||
chat: AIChatState;
|
chat: AIChatState;
|
||||||
trademark: boolean;
|
trademark: boolean;
|
||||||
|
welcomeMessage?: string;
|
||||||
|
suggestions?: string[];
|
||||||
}) {
|
}) {
|
||||||
const { chatController, chat, trademark } = props;
|
const { chatController, chat, trademark, suggestions } = props;
|
||||||
|
|
||||||
const [input, setInput] = React.useState('');
|
const [input, setInput] = React.useState('');
|
||||||
|
|
||||||
@@ -246,7 +247,10 @@ export function AIChatBody(props: {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{!chat.error ? (
|
{!chat.error ? (
|
||||||
<AIChatSuggestedQuestions chatController={chatController} />
|
<AIChatSuggestedQuestions
|
||||||
|
chatController={chatController}
|
||||||
|
suggestions={suggestions}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useLanguage } from '@/intl/client';
|
||||||
|
import { t, tString } from '@/intl/translate';
|
||||||
|
import { Icon } from '@gitbook/icons';
|
||||||
|
import { useAIChatController, useAIChatState } from '../AI';
|
||||||
|
import { Button, DropdownMenu, DropdownMenuItem } from '../primitives';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button to control the chat (clear, etc.)
|
||||||
|
*/
|
||||||
|
export function AIChatControlButton() {
|
||||||
|
const language = useLanguage();
|
||||||
|
const chat = useAIChatState();
|
||||||
|
const chatController = useAIChatController();
|
||||||
|
|
||||||
|
return chat.messages.length > 0 ? (
|
||||||
|
<DropdownMenu
|
||||||
|
button={
|
||||||
|
<Button
|
||||||
|
onClick={() => {}}
|
||||||
|
iconOnly
|
||||||
|
icon="ellipsis"
|
||||||
|
label={tString(language, 'actions')}
|
||||||
|
variant="blank"
|
||||||
|
size="default"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
chatController.clear();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="broom-wide" className="size-3 shrink-0 text-tint-subtle" />
|
||||||
|
{t(language, 'ai_chat_clear_conversation')}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenu>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
@@ -2,19 +2,23 @@ import { tString, useLanguage } from '@/intl/client';
|
|||||||
import type { AIChatController } from '../AI';
|
import type { AIChatController } from '../AI';
|
||||||
import { Button } from '../primitives';
|
import { Button } from '../primitives';
|
||||||
|
|
||||||
export default function AIChatSuggestedQuestions(props: { chatController: AIChatController }) {
|
export default function AIChatSuggestedQuestions(props: {
|
||||||
const { chatController } = props;
|
chatController: AIChatController;
|
||||||
|
suggestions?: string[];
|
||||||
|
}) {
|
||||||
const language = useLanguage();
|
const language = useLanguage();
|
||||||
|
const {
|
||||||
const DEFAULT_SUGGESTED_QUESTIONS = [
|
chatController,
|
||||||
tString(language, 'ai_chat_suggested_questions_about_this_page'),
|
suggestions = [
|
||||||
tString(language, 'ai_chat_suggested_questions_read_next'),
|
tString(language, 'ai_chat_suggested_questions_about_this_page'),
|
||||||
tString(language, 'ai_chat_suggested_questions_example'),
|
tString(language, 'ai_chat_suggested_questions_read_next'),
|
||||||
];
|
tString(language, 'ai_chat_suggested_questions_example'),
|
||||||
|
],
|
||||||
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
{DEFAULT_SUGGESTED_QUESTIONS.map((question, index) => (
|
{suggestions.map((question, index) => (
|
||||||
<Button
|
<Button
|
||||||
key={question}
|
key={question}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ export * from './AIChat';
|
|||||||
export * from './AIChatButton';
|
export * from './AIChatButton';
|
||||||
export * from './AIChatIcon';
|
export * from './AIChatIcon';
|
||||||
export * from './AIResponseFeedback';
|
export * from './AIResponseFeedback';
|
||||||
|
export * from './AIChatControlButton';
|
||||||
|
|||||||
@@ -1,7 +1,21 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useAIChatController, useAIChatState } from '@/components/AI';
|
import { useAIChatController, useAIChatState } from '@/components/AI';
|
||||||
import { AIChatBody } from '@/components/AIChat';
|
import {
|
||||||
|
AIChatBody,
|
||||||
|
AIChatControlButton,
|
||||||
|
AIChatDynamicIcon,
|
||||||
|
AIChatSubtitle,
|
||||||
|
} from '@/components/AIChat';
|
||||||
|
import {
|
||||||
|
EmbeddableFrame,
|
||||||
|
EmbeddableFrameBody,
|
||||||
|
EmbeddableFrameButtons,
|
||||||
|
EmbeddableFrameHeader,
|
||||||
|
EmbeddableFrameHeaderMain,
|
||||||
|
EmbeddableFrameTitle,
|
||||||
|
} from './EmbeddableFrame';
|
||||||
|
import { EmbeddableIframeButtons, useEmbeddableConfiguration } from './EmbeddableIframeAPI';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Embeddable AI chat window in an iframe.
|
* Embeddable AI chat window in an iframe.
|
||||||
@@ -12,6 +26,29 @@ export function EmbeddableAIChat(props: {
|
|||||||
const { trademark } = props;
|
const { trademark } = props;
|
||||||
const chat = useAIChatState();
|
const chat = useAIChatState();
|
||||||
const chatController = useAIChatController();
|
const chatController = useAIChatController();
|
||||||
|
const configuration = useEmbeddableConfiguration();
|
||||||
|
|
||||||
return <AIChatBody trademark={trademark} chatController={chatController} chat={chat} />;
|
return (
|
||||||
|
<EmbeddableFrame>
|
||||||
|
<EmbeddableFrameHeader>
|
||||||
|
<AIChatDynamicIcon trademark={trademark} />
|
||||||
|
<EmbeddableFrameHeaderMain>
|
||||||
|
<EmbeddableFrameTitle>GitBook Assistant</EmbeddableFrameTitle>
|
||||||
|
<AIChatSubtitle chat={chat} />
|
||||||
|
</EmbeddableFrameHeaderMain>
|
||||||
|
<EmbeddableFrameButtons>
|
||||||
|
<AIChatControlButton />
|
||||||
|
<EmbeddableIframeButtons />
|
||||||
|
</EmbeddableFrameButtons>
|
||||||
|
</EmbeddableFrameHeader>
|
||||||
|
<EmbeddableFrameBody>
|
||||||
|
<AIChatBody
|
||||||
|
trademark={trademark}
|
||||||
|
chatController={chatController}
|
||||||
|
chat={chat}
|
||||||
|
suggestions={configuration.suggestions}
|
||||||
|
/>
|
||||||
|
</EmbeddableFrameBody>
|
||||||
|
</EmbeddableFrame>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,15 @@ import { type PagePathParams, getSitePageData } from '@/components/SitePage';
|
|||||||
import { PageBody } from '@/components/PageBody';
|
import { PageBody } from '@/components/PageBody';
|
||||||
import type { GitBookSiteContext } from '@/lib/context';
|
import type { GitBookSiteContext } from '@/lib/context';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
import { Button } from '../primitives';
|
||||||
|
import {
|
||||||
|
EmbeddableFrame,
|
||||||
|
EmbeddableFrameBody,
|
||||||
|
EmbeddableFrameButtons,
|
||||||
|
EmbeddableFrameHeader,
|
||||||
|
EmbeddableFrameHeaderMain,
|
||||||
|
} from './EmbeddableFrame';
|
||||||
|
import { EmbeddableIframeButtons } from './EmbeddableIframeAPI';
|
||||||
|
|
||||||
export const dynamic = 'force-static';
|
export const dynamic = 'force-static';
|
||||||
|
|
||||||
@@ -22,15 +31,33 @@ export async function EmbeddableDocsPage(props: EmbeddableDocsPageProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 overflow-auto p-6">
|
<EmbeddableFrame>
|
||||||
<PageBody
|
<EmbeddableFrameHeader>
|
||||||
context={context}
|
<EmbeddableFrameHeaderMain>
|
||||||
page={page}
|
<Button
|
||||||
ancestors={ancestors}
|
href={context.linker.toPathInSite('~gitbook/embed/assistant')}
|
||||||
document={document}
|
size="default"
|
||||||
withPageFeedback={withPageFeedback}
|
variant="blank"
|
||||||
/>
|
icon="arrow-left"
|
||||||
</div>
|
label="Back"
|
||||||
|
/>
|
||||||
|
</EmbeddableFrameHeaderMain>
|
||||||
|
<EmbeddableFrameButtons>
|
||||||
|
<EmbeddableIframeButtons />
|
||||||
|
</EmbeddableFrameButtons>
|
||||||
|
</EmbeddableFrameHeader>
|
||||||
|
<EmbeddableFrameBody>
|
||||||
|
<div className="flex-1 overflow-auto p-6">
|
||||||
|
<PageBody
|
||||||
|
context={context}
|
||||||
|
page={page}
|
||||||
|
ancestors={ancestors}
|
||||||
|
document={document}
|
||||||
|
withPageFeedback={withPageFeedback}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</EmbeddableFrameBody>
|
||||||
|
</EmbeddableFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,17 @@
|
|||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export type EmbeddableFrameHeaderProps = {
|
export type EmbeddableFrameProps = React.ComponentProps<'div'> & {
|
||||||
icon: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
|
||||||
title: string;
|
|
||||||
subtitle?: string;
|
|
||||||
|
|
||||||
buttons?: React.ReactNode;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EmbeddableFrameProps = EmbeddableFrameHeaderProps &
|
|
||||||
React.ComponentProps<'div'> & {
|
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Presentation component to display an embeddable frame.
|
* Presentation component to display an embeddable frame.
|
||||||
* It is used for the AI chat window in the docs, but also when embedded in another website.
|
* It is used for the AI chat window in the docs, but also when embedded in another website.
|
||||||
*/
|
*/
|
||||||
export const EmbeddableFrame = React.forwardRef<HTMLDivElement, EmbeddableFrameProps>(
|
export const EmbeddableFrame = React.forwardRef<HTMLDivElement, EmbeddableFrameProps>(
|
||||||
(props, ref) => {
|
(props, ref) => {
|
||||||
const { icon, title, subtitle, buttons, children, ...divProps } = props;
|
const { children, ...divProps } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -32,22 +22,70 @@ export const EmbeddableFrame = React.forwardRef<HTMLDivElement, EmbeddableFrameP
|
|||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
<div className="flex select-none items-center gap-2 border-tint-subtle border-b bg-tint-subtle px-4 py-2 text-tint-strong">
|
{children}
|
||||||
{icon}
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div className="font-bold">{title}</div>
|
|
||||||
<div
|
|
||||||
className={`text-tint text-xs leading-none transition-all duration-500 ${
|
|
||||||
subtitle ? 'h-3 opacity-11' : 'h-0 opacity-0'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{subtitle}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="ml-auto flex gap-2">{buttons}</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-1 flex-col overflow-hidden">{children}</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export function EmbeddableFrameHeader(props: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const { children } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex select-none items-center gap-2 border-tint-subtle border-b bg-tint-subtle px-4 py-2 text-tint-strong">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmbeddableFrameHeaderMain(props: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const { children } = props;
|
||||||
|
|
||||||
|
return <div className="flex flex-1 flex-col">{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmbeddableFrameBody(props: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const { children } = props;
|
||||||
|
|
||||||
|
return <div className="flex flex-1 flex-col overflow-hidden">{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmbeddableFrameTitle(props: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const { children } = props;
|
||||||
|
|
||||||
|
return <div className="font-bold">{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmbeddableFrameSubtitle(props: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const { children, className } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={tcls(
|
||||||
|
'text-tint text-xs leading-none transition-all duration-500',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmbeddableFrameButtons(props: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const { children } = props;
|
||||||
|
|
||||||
|
return <div className="ml-auto flex gap-2">{children}</div>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,31 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { ParentToFrameMessage } from '@gitbook/embed';
|
import type { GitBookEmbeddableConfiguration, ParentToFrameMessage } from '@gitbook/embed';
|
||||||
import { createChannel } from 'bidc';
|
import { createChannel } from 'bidc';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { useAIChatController } from '@/components/AI';
|
import { useAIChatController } from '@/components/AI';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { createStore, useStore } from 'zustand';
|
||||||
|
import { integrationsAssistantTools } from '../Integrations';
|
||||||
|
import { Button } from '../primitives';
|
||||||
|
|
||||||
|
const embeddableConfiguration = createStore<GitBookEmbeddableConfiguration>(() => ({
|
||||||
|
buttons: [],
|
||||||
|
welcomeMessage: '',
|
||||||
|
suggestions: [],
|
||||||
|
tools: [],
|
||||||
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expose the API to communicate with the parent window.
|
* Expose the API to communicate with the parent window.
|
||||||
*/
|
*/
|
||||||
export function EmbeddableIframeAPI() {
|
export function EmbeddableIframeAPI(props: {
|
||||||
|
baseURL: string;
|
||||||
|
}) {
|
||||||
|
const { baseURL } = props;
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
const chatController = useAIChatController();
|
const chatController = useAIChatController();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -17,11 +33,14 @@ export function EmbeddableIframeAPI() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[gitbook] create channel with parent window');
|
||||||
const channel = createChannel();
|
const channel = createChannel();
|
||||||
|
|
||||||
channel.receive((payload) => {
|
channel.receive((payload) => {
|
||||||
const message = payload as ParentToFrameMessage;
|
const message = payload as ParentToFrameMessage;
|
||||||
|
|
||||||
|
console.log('[gitbook] received message', message);
|
||||||
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'clearChat': {
|
case 'clearChat': {
|
||||||
chatController.clear();
|
chatController.clear();
|
||||||
@@ -33,12 +52,64 @@ export function EmbeddableIframeAPI() {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// TODO: Handle other messages
|
case 'configure': {
|
||||||
|
embeddableConfiguration.setState(message.settings);
|
||||||
|
integrationsAssistantTools.setState({
|
||||||
|
tools: message.settings.tools,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'navigateToPage': {
|
||||||
|
router.push(`${baseURL}/page/${message.pagePath}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'navigateToAssistant': {
|
||||||
|
router.push(`${baseURL}/assistant`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return channel.cleanup();
|
return () => {
|
||||||
}, [chatController]);
|
console.log('[gitbook] cleanup');
|
||||||
|
channel.cleanup();
|
||||||
|
};
|
||||||
|
}, [chatController, router, baseURL]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to get the configuration from the parent window.
|
||||||
|
*/
|
||||||
|
export function useEmbeddableConfiguration<T = GitBookEmbeddableConfiguration>(
|
||||||
|
// @ts-expect-error - This is a workaround to allow the function to be optional.
|
||||||
|
fn: (state: GitBookEmbeddableConfiguration) => T = (state) => state
|
||||||
|
) {
|
||||||
|
return useStore(embeddableConfiguration, fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the buttons defined by the parent window.
|
||||||
|
*/
|
||||||
|
export function EmbeddableIframeButtons() {
|
||||||
|
const buttons = useEmbeddableConfiguration((state) => state.buttons);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{buttons.map((button) => (
|
||||||
|
<Button
|
||||||
|
key={button.label}
|
||||||
|
size="default"
|
||||||
|
variant="blank"
|
||||||
|
icon={button.icon}
|
||||||
|
label={button.label}
|
||||||
|
iconOnly
|
||||||
|
onClick={() => {
|
||||||
|
button.onClick();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { AIChatProvider, AIContextProvider } from '@/components/AI';
|
import { AIChatProvider, AIContextProvider } from '@/components/AI';
|
||||||
import { AIChatDynamicIcon } from '@/components/AIChat';
|
|
||||||
import { EmbeddableFrame } from '@/components/Embeddable';
|
|
||||||
import { CustomizationRootLayout } from '@/components/RootLayout';
|
import { CustomizationRootLayout } from '@/components/RootLayout';
|
||||||
import {
|
import {
|
||||||
SiteLayoutClientContexts,
|
SiteLayoutClientContexts,
|
||||||
@@ -29,26 +27,22 @@ export async function EmbeddableRootLayout({
|
|||||||
externalLinksTarget={context.customization.externalLinks.target}
|
externalLinksTarget={context.customization.externalLinks.target}
|
||||||
contextId={context.contextId}
|
contextId={context.contextId}
|
||||||
>
|
>
|
||||||
<EmbeddableFrame
|
<AIContextProvider
|
||||||
icon={<AIChatDynamicIcon trademark={context.customization.trademark.enabled} />}
|
aiMode={CustomizationAIMode.Assistant}
|
||||||
title="Test"
|
trademark={context.customization.trademark.enabled}
|
||||||
className="fixed inset-0"
|
|
||||||
>
|
>
|
||||||
<AIContextProvider
|
<AIChatProvider
|
||||||
aiMode={CustomizationAIMode.Assistant}
|
renderMessageOptions={{
|
||||||
trademark={context.customization.trademark.enabled}
|
withLinkPreviews: false,
|
||||||
|
asEmbeddable: true,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<AIChatProvider
|
<div className="fixed inset-0 flex flex-col">{children}</div>
|
||||||
renderMessageOptions={{
|
<EmbeddableIframeAPI
|
||||||
withLinkPreviews: false,
|
baseURL={context.linker.toPathInSpace('~gitbook/embed/')}
|
||||||
asEmbeddable: true,
|
/>
|
||||||
}}
|
</AIChatProvider>
|
||||||
>
|
</AIContextProvider>
|
||||||
{children}
|
|
||||||
<EmbeddableIframeAPI />
|
|
||||||
</AIChatProvider>
|
|
||||||
</AIContextProvider>
|
|
||||||
</EmbeddableFrame>
|
|
||||||
</SiteLayoutClientContexts>
|
</SiteLayoutClientContexts>
|
||||||
</CustomizationRootLayout>
|
</CustomizationRootLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user