Embeddable GitBook API (#3593)

This commit is contained in:
Samy Pessé
2025-08-26 14:08:24 +02:00
committed by GitHub
parent ada195d329
commit 81a6bd756a
19 changed files with 480 additions and 192 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@gitbook/embed": minor
---
Improve API to control the GitBook embed
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": minor
---
Support customization of buttons and tools through iframe API
+1
View File
@@ -18,6 +18,7 @@
"**/.vercel/**/*",
"**/.cache/**/*",
"**/.wrangler/**/*",
"packages/embed/standalone/**/*",
"packages/openapi-parser/src/fixtures/**/*",
"packages/emoji-codepoints/index.ts",
"packages/icons/src/data/*.json",
+15 -10
View File
@@ -1,8 +1,7 @@
import { createChannel } from 'bidc';
import type {
FrameToParentMessage,
GitBookPlaceholderSettings,
GitBookToolDefinition,
GitBookEmbeddableConfiguration,
ParentToFrameMessage,
} from './protocol';
@@ -22,11 +21,6 @@ export type GitBookFrameClient = {
*/
postUserMessage: (message: string) => void;
/**
* Register a custom tool.
*/
registerTool: (tool: GitBookToolDefinition) => void;
/**
* Clear the chat.
*/
@@ -35,7 +29,7 @@ export type GitBookFrameClient = {
/**
* Set the placeholder settings.
*/
setPlaceholder: (placeholder: GitBookPlaceholderSettings) => void;
configure: (settings: Partial<GitBookEmbeddableConfiguration>) => void;
/**
* Register an event listener.
@@ -53,6 +47,7 @@ export function createGitBookFrame(iframe: HTMLIFrameElement): GitBookFrameClien
const channel = createChannel(iframe.contentWindow);
channel.receive((message: FrameToParentMessage) => {
console.log('[gitbook:embed] received message', message);
if (message.type === 'close') {
const listeners = events.get('close') || [];
if (listeners) {
@@ -62,11 +57,19 @@ export function createGitBookFrame(iframe: HTMLIFrameElement): GitBookFrameClien
});
const sendToFrame = (message: ParentToFrameMessage) => {
console.log('[gitbook:embed] send message', message);
channel.send(message);
};
const events = new Map<string, Array<(...args: any[]) => void>>();
const configuration: GitBookEmbeddableConfiguration = {
buttons: [],
welcomeMessage: '',
suggestions: [],
tools: [],
};
return {
navigateToPage: (pagePath) => {
sendToFrame({ type: 'navigateToPage', pagePath });
@@ -75,9 +78,11 @@ export function createGitBookFrame(iframe: HTMLIFrameElement): GitBookFrameClien
sendToFrame({ type: 'navigateToAssistant' });
},
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' }),
setPlaceholder: (settings) => sendToFrame({ type: 'setPlaceholder', settings }),
on: (event, listener) => {
const listeners = events.get(event) || [];
listeners.push(listener);
+31 -11
View File
@@ -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[];
/** Tools to be provided to the assistant. */
tools: GitBookToolDefinition[];
};
/**
@@ -43,16 +67,12 @@ export type ParentToFrameMessage =
type: 'postUserMessage';
message: string;
}
| {
type: 'registerTool';
tool: GitBookToolDefinition;
}
| {
type: 'clearChat';
}
| {
type: 'setPlaceholder';
settings: GitBookPlaceholderSettings;
type: 'configure';
settings: GitBookEmbeddableConfiguration;
}
| {
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>
);
}
+51
View File
@@ -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
View File
@@ -1 +1,2 @@
export * from './GitBookProvider';
export * from './GitBookFrame';
+31 -11
View File
@@ -4,9 +4,8 @@ import {
type CreateGitBookOptions,
type GetFrameURLOptions,
type GitBookClient,
type GitBookEmbeddableConfiguration,
type GitBookFrameClient,
type GitBookPlaceholderSettings,
type GitBookToolDefinition,
createGitBook,
} from '../client';
@@ -29,12 +28,10 @@ type StandaloneCalls =
| ['toggle']
// Post a user message
| ['postUserMessage', string]
// Register a tool
| ['registerTool', GitBookToolDefinition]
// Clear the chat
| ['clearChat']
// Configure the placeholder
| ['setPlaceholder', GitBookPlaceholderSettings]
// Configure the embed
| ['configure', Partial<GitBookEmbeddableConfiguration>]
// Navigate to a page
| ['navigateToPage', string]
// Navigate to the assistant
@@ -65,6 +62,12 @@ let widgetIframe: HTMLIFrameElement | undefined;
let _client: GitBookClient | undefined;
let _frame: GitBookFrameClient | undefined;
let frameOptions: GetFrameURLOptions | undefined;
let frameConfiguration: GitBookEmbeddableConfiguration = {
buttons: [],
welcomeMessage: '',
suggestions: [],
tools: [],
};
function getClient() {
if (!_client) {
@@ -132,15 +135,30 @@ const GitBook = (...args: StandaloneCalls) => {
case 'postUserMessage':
getIframe().frame.postUserMessage(args[1]);
break;
case 'registerTool':
getIframe().frame.registerTool(args[1]);
case 'configure':
frameConfiguration = {
...frameConfiguration,
...args[1],
};
getIframe().frame.configure({
...frameConfiguration,
buttons: [
...frameConfiguration.buttons,
// Always include a close button
{
icon: 'close',
label: 'Close',
onClick: () => {
GitBook('close');
},
},
],
});
break;
case 'clearChat':
getIframe().frame.clearChat();
break;
case 'setPlaceholder':
getIframe().frame.setPlaceholder(args[1]);
break;
case 'navigateToPage':
getIframe().frame.navigateToPage(args[1]);
break;
@@ -156,3 +174,5 @@ const precalls = (window.GitBook as GitBookStandalone | undefined)?.q ?? [];
// @ts-expect-error - GitBook is not defined in the global scope
window.GitBook = GitBook;
precalls.forEach((call) => GitBook(...call));
GitBook('configure', {});
@@ -346,7 +346,8 @@ export function AIChatProvider(props: {
loading: false,
error: false,
}));
} catch {
} catch (error) {
console.error('Error streaming AI response', error);
globalState.setState((state) => ({
...state,
loading: false,
@@ -359,6 +360,7 @@ export function AIChatProvider(props: {
renderMessageOptions?.withLinkPreviews,
renderMessageOptions?.withToolCalls,
renderMessageOptions?.asEmbeddable,
language,
]
);
@@ -11,10 +11,18 @@ import {
useAIChatController,
useAIChatState,
} from '../AI';
import { EmbeddableFrame } from '../Embeddable/EmbeddableFrame';
import {
EmbeddableFrame,
EmbeddableFrameBody,
EmbeddableFrameButtons,
EmbeddableFrameHeader,
EmbeddableFrameHeaderMain,
EmbeddableFrameSubtitle,
EmbeddableFrameTitle,
} from '../Embeddable/EmbeddableFrame';
import { useNow } from '../hooks';
import { Button } from '../primitives';
import { DropdownMenu, DropdownMenuItem } from '../primitives';
import { AIChatControlButton } from './AIChatControlButton';
import { AIChatIcon } from './AIChatIcon';
import { AIChatInput } from './AIChatInput';
import { AIChatMessages } from './AIChatMessages';
@@ -66,45 +74,17 @@ export function AIChat(props: { trademark: boolean }) {
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"
>
<EmbeddableFrame
className="relative circular-corners:rounded-3xl rounded-corners:rounded-md depth-subtle:shadow-lg shadow-tint ring-1 ring-tint-subtle"
icon={<AIChatDynamicIcon trademark={trademark} />}
title={getAIChatName(language, trademark)}
subtitle={
chat.loading
? chat.messages[chat.messages.length - 1].content
? tString(language, 'ai_chat_working')
: tString(language, 'ai_chat_thinking')
: ''
}
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}
<EmbeddableFrame className="relative circular-corners:rounded-3xl rounded-corners:rounded-md depth-subtle:shadow-lg shadow-tint ring-1 ring-tint-subtle">
<EmbeddableFrameHeader>
<AIChatDynamicIcon trademark={trademark} />
<EmbeddableFrameHeaderMain>
<EmbeddableFrameTitle>
{getAIChatName(language, trademark)}
</EmbeddableFrameTitle>
<AIChatSubtitle chat={chat} />
</EmbeddableFrameHeaderMain>
<EmbeddableFrameButtons>
<AIChatControlButton />
<Button
onClick={() => chatController.close()}
iconOnly
@@ -113,10 +93,11 @@ export function AIChat(props: { trademark: boolean }) {
variant="blank"
size="default"
/>
</>
}
>
<AIChatBody chatController={chatController} chat={chat} trademark={trademark} />
</EmbeddableFrameButtons>
</EmbeddableFrameHeader>
<EmbeddableFrameBody>
<AIChatBody chatController={chatController} chat={chat} trademark={trademark} />
</EmbeddableFrameBody>
</EmbeddableFrame>
</div>
);
@@ -139,7 +120,7 @@ export function AIChatDynamicIcon(props: {
chat.error
? 'error'
: chat.loading
? chat.messages[chat.messages.length - 1].content
? chat.messages[chat.messages.length - 1]?.content
? 'working'
: 'thinking'
: 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.
*/
@@ -159,8 +158,10 @@ export function AIChatBody(props: {
chatController: AIChatController;
chat: AIChatState;
trademark: boolean;
welcomeMessage?: string;
suggestions?: string[];
}) {
const { chatController, chat, trademark } = props;
const { chatController, chat, trademark, suggestions } = props;
const [input, setInput] = React.useState('');
@@ -246,7 +247,10 @@ export function AIChatBody(props: {
</p>
</div>
{!chat.error ? (
<AIChatSuggestedQuestions chatController={chatController} />
<AIChatSuggestedQuestions
chatController={chatController}
suggestions={suggestions}
/>
) : null}
</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 { Button } from '../primitives';
export default function AIChatSuggestedQuestions(props: { chatController: AIChatController }) {
const { chatController } = props;
export default function AIChatSuggestedQuestions(props: {
chatController: AIChatController;
suggestions?: string[];
}) {
const language = useLanguage();
const DEFAULT_SUGGESTED_QUESTIONS = [
tString(language, 'ai_chat_suggested_questions_about_this_page'),
tString(language, 'ai_chat_suggested_questions_read_next'),
tString(language, 'ai_chat_suggested_questions_example'),
];
const {
chatController,
suggestions = [
tString(language, 'ai_chat_suggested_questions_about_this_page'),
tString(language, 'ai_chat_suggested_questions_read_next'),
tString(language, 'ai_chat_suggested_questions_example'),
],
} = props;
return (
<div className="flex flex-col items-center gap-2">
{DEFAULT_SUGGESTED_QUESTIONS.map((question, index) => (
{suggestions.map((question, index) => (
<Button
key={question}
variant="secondary"
@@ -2,3 +2,4 @@ export * from './AIChat';
export * from './AIChatButton';
export * from './AIChatIcon';
export * from './AIResponseFeedback';
export * from './AIChatControlButton';
@@ -1,7 +1,21 @@
'use client';
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.
@@ -12,6 +26,29 @@ export function EmbeddableAIChat(props: {
const { trademark } = props;
const chat = useAIChatState();
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 type { GitBookSiteContext } from '@/lib/context';
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';
@@ -22,15 +31,33 @@ export async function EmbeddableDocsPage(props: EmbeddableDocsPageProps) {
});
return (
<div className="flex-1 overflow-auto p-6">
<PageBody
context={context}
page={page}
ancestors={ancestors}
document={document}
withPageFeedback={withPageFeedback}
/>
</div>
<EmbeddableFrame>
<EmbeddableFrameHeader>
<EmbeddableFrameHeaderMain>
<Button
href={context.linker.toPathInSite('~gitbook/embed/assistant')}
size="default"
variant="blank"
icon="arrow-left"
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 React from 'react';
export type EmbeddableFrameHeaderProps = {
icon: React.ReactNode;
title: string;
subtitle?: string;
buttons?: React.ReactNode;
export type EmbeddableFrameProps = React.ComponentProps<'div'> & {
children: React.ReactNode;
};
export type EmbeddableFrameProps = EmbeddableFrameHeaderProps &
React.ComponentProps<'div'> & {
children: React.ReactNode;
};
/**
* 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.
*/
export const EmbeddableFrame = React.forwardRef<HTMLDivElement, EmbeddableFrameProps>(
(props, ref) => {
const { icon, title, subtitle, buttons, children, ...divProps } = props;
const { children, ...divProps } = props;
return (
<div
@@ -32,22 +22,70 @@ export const EmbeddableFrame = React.forwardRef<HTMLDivElement, EmbeddableFrameP
)}
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">
{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>
{children}
</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';
import type { ParentToFrameMessage } from '@gitbook/embed';
import type { GitBookEmbeddableConfiguration, ParentToFrameMessage } from '@gitbook/embed';
import { createChannel } from 'bidc';
import React from 'react';
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.
*/
export function EmbeddableIframeAPI() {
export function EmbeddableIframeAPI(props: {
baseURL: string;
}) {
const { baseURL } = props;
const router = useRouter();
const chatController = useAIChatController();
React.useEffect(() => {
@@ -17,11 +33,14 @@ export function EmbeddableIframeAPI() {
return;
}
console.log('[gitbook] create channel with parent window');
const channel = createChannel();
channel.receive((payload) => {
const message = payload as ParentToFrameMessage;
console.log('[gitbook] received message', message);
switch (message.type) {
case 'clearChat': {
chatController.clear();
@@ -33,12 +52,64 @@ export function EmbeddableIframeAPI() {
});
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();
}, [chatController]);
return () => {
console.log('[gitbook] cleanup');
channel.cleanup();
};
}, [chatController, router, baseURL]);
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 { AIChatDynamicIcon } from '@/components/AIChat';
import { EmbeddableFrame } from '@/components/Embeddable';
import { CustomizationRootLayout } from '@/components/RootLayout';
import {
SiteLayoutClientContexts,
@@ -29,26 +27,22 @@ export async function EmbeddableRootLayout({
externalLinksTarget={context.customization.externalLinks.target}
contextId={context.contextId}
>
<EmbeddableFrame
icon={<AIChatDynamicIcon trademark={context.customization.trademark.enabled} />}
title="Test"
className="fixed inset-0"
<AIContextProvider
aiMode={CustomizationAIMode.Assistant}
trademark={context.customization.trademark.enabled}
>
<AIContextProvider
aiMode={CustomizationAIMode.Assistant}
trademark={context.customization.trademark.enabled}
<AIChatProvider
renderMessageOptions={{
withLinkPreviews: false,
asEmbeddable: true,
}}
>
<AIChatProvider
renderMessageOptions={{
withLinkPreviews: false,
asEmbeddable: true,
}}
>
{children}
<EmbeddableIframeAPI />
</AIChatProvider>
</AIContextProvider>
</EmbeddableFrame>
<div className="fixed inset-0 flex flex-col">{children}</div>
<EmbeddableIframeAPI
baseURL={context.linker.toPathInSpace('~gitbook/embed/')}
/>
</AIChatProvider>
</AIContextProvider>
</SiteLayoutClientContexts>
</CustomizationRootLayout>
);