From 2382e63c74de7835ba4f1aeca1d933d550bd8117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Sun, 17 Aug 2025 10:09:43 +0200 Subject: [PATCH 01/48] Move to (content) --- .../[siteURL]/[siteData]/{ => (content)}/[pagePath]/not-found.tsx | 0 .../[siteURL]/[siteData]/{ => (content)}/[pagePath]/page.tsx | 0 .../static/[mode]/[siteURL]/[siteData]/{ => (content)}/layout.tsx | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/{ => (content)}/[pagePath]/not-found.tsx (100%) rename packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/{ => (content)}/[pagePath]/page.tsx (100%) rename packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/{ => (content)}/layout.tsx (100%) diff --git a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/[pagePath]/not-found.tsx b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/(content)/[pagePath]/not-found.tsx similarity index 100% rename from packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/[pagePath]/not-found.tsx rename to packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/(content)/[pagePath]/not-found.tsx diff --git a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/[pagePath]/page.tsx b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/(content)/[pagePath]/page.tsx similarity index 100% rename from packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/[pagePath]/page.tsx rename to packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/(content)/[pagePath]/page.tsx diff --git a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/layout.tsx b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/(content)/layout.tsx similarity index 100% rename from packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/layout.tsx rename to packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/(content)/layout.tsx From f76d01bd065e36ee400075a292f3082de7ed1173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Sun, 17 Aug 2025 10:25:39 +0200 Subject: [PATCH 02/48] Start --- .../~gitbook/embed/assistant/layout.tsx | 30 +++++++++++++++++++ .../~gitbook/embed/assistant/page.tsx | 18 +++++++++++ packages/gitbook/src/middleware.ts | 1 + 3 files changed, 49 insertions(+) create mode 100644 packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/layout.tsx create mode 100644 packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx diff --git a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/layout.tsx b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/layout.tsx new file mode 100644 index 0000000000..e84bf3c5ea --- /dev/null +++ b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/layout.tsx @@ -0,0 +1,30 @@ +import { type RouteLayoutParams, getStaticSiteContext } from '@/app/utils'; +import { CustomizationRootLayout } from '@/components/RootLayout'; +import { generateSiteLayoutMetadata, generateSiteLayoutViewport } from '@/components/SiteLayout'; + +interface SiteStaticLayoutProps { + params: Promise; +} + +export default async function EmbedAssistantRootLayout({ + params, + children, +}: React.PropsWithChildren) { + const { context } = await getStaticSiteContext(await params); + + return ( + + {children} + + ); +} + +export async function generateViewport({ params }: SiteStaticLayoutProps) { + const { context } = await getStaticSiteContext(await params); + return generateSiteLayoutViewport(context); +} + +export async function generateMetadata({ params }: SiteStaticLayoutProps) { + const { context } = await getStaticSiteContext(await params); + return generateSiteLayoutMetadata(context); +} diff --git a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx new file mode 100644 index 0000000000..102e8d5cd1 --- /dev/null +++ b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx @@ -0,0 +1,18 @@ +import { type RouteLayoutParams, getStaticSiteContext } from '@/app/utils'; + +export const dynamic = 'force-static'; + +type PageProps = { + params: Promise; +}; + +export default async function EmbedAssistantPage(props: PageProps) { + const params = await props.params; + const { context } = await getStaticSiteContext(params); + + return ( +
+

Hello

+
+ ); +} diff --git a/packages/gitbook/src/middleware.ts b/packages/gitbook/src/middleware.ts index 6237903a57..a7ea7d6121 100644 --- a/packages/gitbook/src/middleware.ts +++ b/packages/gitbook/src/middleware.ts @@ -538,6 +538,7 @@ function encodePathInSiteContent(rawPathname: string): { case 'sitemap.xml': case 'sitemap-pages.xml': case 'robots.txt': + case '~gitbook/embed/assistant': // LLMs.txt, sitemap, sitemap-pages and robots.txt are always static // as they only depend on the site structure / pages. return { pathname, routeType: 'static' }; From b20f4544c787c486d2985369f2f5a6981807e8c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Sun, 17 Aug 2025 10:35:28 +0200 Subject: [PATCH 03/48] Render the assistant --- .../~gitbook/embed/assistant/layout.tsx | 3 +- .../~gitbook/embed/assistant/page.tsx | 7 +--- .../gitbook/src/components/AIChat/AIChat.tsx | 42 +++++++++++++++---- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/layout.tsx b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/layout.tsx index e84bf3c5ea..e45ba884ca 100644 --- a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/layout.tsx +++ b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/layout.tsx @@ -1,6 +1,7 @@ import { type RouteLayoutParams, getStaticSiteContext } from '@/app/utils'; import { CustomizationRootLayout } from '@/components/RootLayout'; import { generateSiteLayoutMetadata, generateSiteLayoutViewport } from '@/components/SiteLayout'; +import { NuqsAdapter } from 'nuqs/adapters/next/app'; interface SiteStaticLayoutProps { params: Promise; @@ -14,7 +15,7 @@ export default async function EmbedAssistantRootLayout({ return ( - {children} + {children} ); } diff --git a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx index 102e8d5cd1..49be6cac36 100644 --- a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx +++ b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx @@ -1,4 +1,5 @@ import { type RouteLayoutParams, getStaticSiteContext } from '@/app/utils'; +import { AIChatEmbed } from '@/components/AIChat'; export const dynamic = 'force-static'; @@ -10,9 +11,5 @@ export default async function EmbedAssistantPage(props: PageProps) { const params = await props.params; const { context } = await getStaticSiteContext(params); - return ( -
-

Hello

-
- ); + return ; } diff --git a/packages/gitbook/src/components/AIChat/AIChat.tsx b/packages/gitbook/src/components/AIChat/AIChat.tsx index 80547bf970..eeffa6cbb1 100644 --- a/packages/gitbook/src/components/AIChat/AIChat.tsx +++ b/packages/gitbook/src/components/AIChat/AIChat.tsx @@ -1,6 +1,7 @@ 'use client'; import { t, tString, useLanguage } from '@/intl/client'; +import { tcls } from '@/lib/tailwind'; import { Icon } from '@gitbook/icons'; import React from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -44,15 +45,44 @@ export function AIChat(props: { trademark: boolean }) { return null; } - return ; + return ( + + ); +} + +/** + * Embeddable view of the AI chat window. + */ +export function AIChatEmbed(props: { trademark: boolean }) { + const { trademark } = props; + const chat = useAIChatState(); + const chatController = useAIChatController(); + + return ( + + ); } -export function AIChatWindow(props: { +/** + * Inner view of the AI chat window. + */ +function AIChatWindow(props: { chatController: AIChatController; chat: AIChatState; trademark: boolean; + className?: string; }) { - const { chatController, chat, trademark } = props; + const { chatController, chat, trademark, className } = props; const [input, setInput] = React.useState(''); @@ -119,11 +149,7 @@ export function AIChatWindow(props: { }, []); return ( -
+
Date: Sun, 17 Aug 2025 10:40:40 +0200 Subject: [PATCH 04/48] Improve styling --- .../gitbook/src/components/AIChat/AIChat.tsx | 264 +++++++++--------- 1 file changed, 135 insertions(+), 129 deletions(-) diff --git a/packages/gitbook/src/components/AIChat/AIChat.tsx b/packages/gitbook/src/components/AIChat/AIChat.tsx index eeffa6cbb1..d558e74a67 100644 --- a/packages/gitbook/src/components/AIChat/AIChat.tsx +++ b/packages/gitbook/src/components/AIChat/AIChat.tsx @@ -46,12 +46,14 @@ export function AIChat(props: { trademark: boolean }) { } return ( - +
+ +
); } @@ -64,12 +66,14 @@ export function AIChatEmbed(props: { trademark: boolean }) { const chatController = useAIChatController(); return ( - +
+ +
); } @@ -149,128 +153,130 @@ function AIChatWindow(props: { }, []); return ( -
-
-
- 0 - ? 'done' - : 'default' - } - /> -
-
{getAIChatName(trademark)}
-
- {chat.loading - ? chat.messages[chat.messages.length - 1].content - ? t(language, 'ai_chat_working') - : t(language, 'ai_chat_thinking') - : ''} -
+
+
+ 0 + ? 'done' + : 'default' + } + /> +
+
{getAIChatName(trademark)}
+
+ {chat.loading + ? chat.messages[chat.messages.length - 1].content + ? t(language, 'ai_chat_working') + : t(language, 'ai_chat_thinking') + : ''}
-
- {}} - iconOnly - icon="ellipsis" - label={tString(language, 'actions')} - variant="blank" - size="default" - /> - } +
+
+ {}} + iconOnly + icon="ellipsis" + label={tString(language, 'actions')} + variant="blank" + size="default" + /> + } + > + { + chatController.clear(); + }} + disabled={isEmpty} > - { - chatController.clear(); - }} - disabled={isEmpty} - > - - {t(language, 'ai_chat_clear_conversation')} - - -
+ + {t(language, 'ai_chat_clear_conversation')} + + +
-
- {isEmpty ? ( -
-
- -
-
-
- {timeGreeting} -
-

- {t(language, 'ai_chat_assistant_description')} -

-
- {!chat.error ? ( - - ) : null} +
+
+ {isEmpty ? ( +
+
+
- ) : ( - - )} -
-
- {/* Display an error banner when something went wrong. */} - {chat.error ? : null} - - { - chatController.postMessage({ message: input }); - setInput(''); - }} +
+
+ {timeGreeting} +
+

+ {t(language, 'ai_chat_assistant_description')} +

+
+ {!chat.error ? ( + + ) : null} +
+ ) : ( + -
+ )} +
+
+ {/* Display an error banner when something went wrong. */} + {chat.error ? : null} + + { + chatController.postMessage({ message: input }); + setInput(''); + }} + />
); From a065ddb3f53f30f76f1f90b59039ca92ac3fd65c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Sun, 17 Aug 2025 11:55:59 +0200 Subject: [PATCH 05/48] Start better components --- bun.lock | 5 ++- packages/gitbook/package.json | 3 +- .../~gitbook/embed/assistant/AIEmbedChat.tsx | 38 +++++++++++++++++++ .../~gitbook/embed/assistant/page.tsx | 4 +- .../gitbook/src/components/AIChat/AIChat.tsx | 22 +---------- 5 files changed, 47 insertions(+), 25 deletions(-) create mode 100644 packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/AIEmbedChat.tsx diff --git a/bun.lock b/bun.lock index d02c7cac9c..60f73cb9d2 100644 --- a/bun.lock +++ b/bun.lock @@ -12,7 +12,7 @@ }, "packages/browser-types": { "name": "@gitbook/browser-types", - "version": "0.3.1", + "version": "0.0.0", "dependencies": { "@gitbook/api": "catalog:", "@gitbook/icons": "workspace:", @@ -84,6 +84,7 @@ "@tusbar/cache-control": "^1.0.2", "ai": "^4.2.2", "assert-never": "^1.2.1", + "bidc": "^0.0.2", "bun-types": "^1.1.20", "classnames": "^2.5.1", "direction": "^2.0.1", @@ -1517,6 +1518,8 @@ "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="], + "bidc": ["bidc@0.0.2", "", {}, "sha512-+t2xK8oQEpxYEM+350mMHSx7eZNoOX6SpjqD+p+lDpVwpYVXjkTCfy0N60xyjelZ2AOa8NAg/12uhAX3MuAR0g=="], + "bignumber.js": ["bignumber.js@9.1.2", "", {}, "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug=="], "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index 6da113245e..76425ea2a3 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -69,7 +69,8 @@ "url-join": "^5.0.0", "usehooks-ts": "^3.1.0", "warn-once": "^0.1.1", - "zustand": "^5.0.3" + "zustand": "^5.0.3", + "bidc": "^0.0.2" }, "devDependencies": { "@argos-ci/playwright": "^5.0.9", diff --git a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/AIEmbedChat.tsx b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/AIEmbedChat.tsx new file mode 100644 index 0000000000..54e21bc637 --- /dev/null +++ b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/AIEmbedChat.tsx @@ -0,0 +1,38 @@ +'use client'; + +import { useAIChatController, useAIChatState } from '@/components/AI'; +import { AIChatWindow } from '@/components/AIChat'; +import { createChannel } from 'bidc'; +import React from 'react'; + +/** + * Embeddable AI chat window in an iframe. + */ +export function AIEmbedChat(props: { + trademark: boolean; +}) { + const { trademark } = props; + const chat = useAIChatState(); + const chatController = useAIChatController(); + + React.useEffect(() => { + const channel = createChannel(); + + channel.receive((payload) => { + console.log('got payload', payload); + }); + + return channel.cleanup(); + }, []); + + return ( +
+ +
+ ); +} diff --git a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx index 49be6cac36..5bb2ba11d2 100644 --- a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx +++ b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx @@ -1,5 +1,5 @@ import { type RouteLayoutParams, getStaticSiteContext } from '@/app/utils'; -import { AIChatEmbed } from '@/components/AIChat'; +import { AIEmbedChat } from './AIEmbedChat'; export const dynamic = 'force-static'; @@ -11,5 +11,5 @@ export default async function EmbedAssistantPage(props: PageProps) { const params = await props.params; const { context } = await getStaticSiteContext(params); - return ; + return ; } diff --git a/packages/gitbook/src/components/AIChat/AIChat.tsx b/packages/gitbook/src/components/AIChat/AIChat.tsx index d558e74a67..a746046738 100644 --- a/packages/gitbook/src/components/AIChat/AIChat.tsx +++ b/packages/gitbook/src/components/AIChat/AIChat.tsx @@ -57,30 +57,10 @@ export function AIChat(props: { trademark: boolean }) { ); } -/** - * Embeddable view of the AI chat window. - */ -export function AIChatEmbed(props: { trademark: boolean }) { - const { trademark } = props; - const chat = useAIChatState(); - const chatController = useAIChatController(); - - return ( -
- -
- ); -} - /** * Inner view of the AI chat window. */ -function AIChatWindow(props: { +export function AIChatWindow(props: { chatController: AIChatController; chat: AIChatState; trademark: boolean; From 70ec1b5c02f99f04bab8abead53b6e17d0a671f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Sun, 17 Aug 2025 12:21:33 +0200 Subject: [PATCH 06/48] Start shape for client --- bun.lock | 21 +++++++++- package.json | 3 +- packages/embed/.gitignore | 1 + packages/embed/README.md | 40 +++++++++++++++++++ packages/embed/package.json | 30 ++++++++++++++ packages/embed/src/client/createGitBook.ts | 14 +++++++ .../embed/src/client/createGitBookFrame.ts | 23 +++++++++++ packages/embed/src/client/index.ts | 1 + packages/embed/src/index.ts | 1 + packages/embed/src/react/GitBookProvider.tsx | 30 ++++++++++++++ packages/embed/src/react/context.ts | 6 +++ packages/embed/src/react/index.ts | 1 + packages/embed/src/standalone.ts | 0 packages/embed/tsconfig.json | 22 ++++++++++ packages/gitbook/package.json | 2 +- 15 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 packages/embed/.gitignore create mode 100644 packages/embed/README.md create mode 100644 packages/embed/package.json create mode 100644 packages/embed/src/client/createGitBook.ts create mode 100644 packages/embed/src/client/createGitBookFrame.ts create mode 100644 packages/embed/src/client/index.ts create mode 100644 packages/embed/src/index.ts create mode 100644 packages/embed/src/react/GitBookProvider.tsx create mode 100644 packages/embed/src/react/context.ts create mode 100644 packages/embed/src/react/index.ts create mode 100644 packages/embed/src/standalone.ts create mode 100644 packages/embed/tsconfig.json diff --git a/bun.lock b/bun.lock index 60f73cb9d2..1ec78dd316 100644 --- a/bun.lock +++ b/bun.lock @@ -39,6 +39,22 @@ "typescript": "^5.5.3", }, }, + "packages/embed": { + "name": "@gitbook/embed", + "version": "0.0.0", + "dependencies": { + "@gitbook/api": "catalog:", + "@gitbook/icons": "workspace:", + "bidc": "catalog:", + }, + "devDependencies": { + "react": "^19.0.0", + "typescript": "^5.5.3", + }, + "peerDependencies": { + "react": "^18.0.0", + }, + }, "packages/emoji-codepoints": { "name": "@gitbook/emoji-codepoints", "version": "0.2.0", @@ -84,7 +100,7 @@ "@tusbar/cache-control": "^1.0.2", "ai": "^4.2.2", "assert-never": "^1.2.1", - "bidc": "^0.0.2", + "bidc": "catalog:", "bun-types": "^1.1.20", "classnames": "^2.5.1", "direction": "^2.0.1", @@ -260,6 +276,7 @@ }, "catalog": { "@gitbook/api": "^0.136.0", + "bidc": "^0.0.2", }, "packages": { "@ai-sdk/provider": ["@ai-sdk/provider@1.1.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="], @@ -632,6 +649,8 @@ "@gitbook/colors": ["@gitbook/colors@workspace:packages/colors"], + "@gitbook/embed": ["@gitbook/embed@workspace:packages/embed"], + "@gitbook/emoji-codepoints": ["@gitbook/emoji-codepoints@workspace:packages/emoji-codepoints"], "@gitbook/fontawesome-pro": ["@gitbook/fontawesome-pro@1.0.8", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "^6.6.0" } }, "sha512-i4PgiuGyUb52Muhc52kK3aMJIMfMkA2RbPW30tre8a6M8T6mWTfYo6gafSgjNvF1vH29zcuB8oBYnF0gO4XcHA=="], diff --git a/package.json b/package.json index 2e08e717ac..2cbc9d3174 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "workspaces": { "packages": ["packages/*"], "catalog": { - "@gitbook/api": "^0.136.0" + "@gitbook/api": "^0.136.0", + "bidc": "^0.0.2" } }, "patchedDependencies": { diff --git a/packages/embed/.gitignore b/packages/embed/.gitignore new file mode 100644 index 0000000000..849ddff3b7 --- /dev/null +++ b/packages/embed/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/packages/embed/README.md b/packages/embed/README.md new file mode 100644 index 0000000000..64cabbaf0c --- /dev/null +++ b/packages/embed/README.md @@ -0,0 +1,40 @@ +# `@gitbook/embed` + +Embed the GitBook Docs Assistant in your product or website. + +# Usage + +## As a script from your docs site + +All GitBook docs site includes a script to easily embed the docs assistant as a widget on your website. + +The script is served at `https://docs.company.com/~gitbook/embed/script.js`. + +You can find the embed script from your docs site settings, or you can copy the following and replace the `docs.company.com` by your docs site hostname. + +```html + +``` + +## As a package from NPM + +Install the package: `npm install @gitbook/embed` and import it in your web application: + +```tsx +import { GitBook } from '@gitbook/embed'; + +``` + +## As React components + +After installing the NPM package, you can import prebuilt React components: + +```tsx +import { GitBookProvider, GitBookChatView } from '@gitbook/embed/react'; + + + + +``` diff --git a/packages/embed/package.json b/packages/embed/package.json new file mode 100644 index 0000000000..e19e2b4642 --- /dev/null +++ b/packages/embed/package.json @@ -0,0 +1,30 @@ +{ + "name": "@gitbook/embed", + "description": "Embeddable components for GitBook", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js", + "standalone": "./dist/standalone.js" + } + }, + "version": "0.0.0", + "dependencies": { + "@gitbook/api": "catalog:", + "@gitbook/icons": "workspace:", + "bidc": "catalog:" + }, + "peerDependencies": { + "react": "^18.0.0" + }, + "devDependencies": { + "typescript": "^5.5.3", + "react": "^19.0.0" + }, + "scripts": { + "build": "tsc", + "typecheck": "tsc --noEmit" + }, + "files": ["dist", "README.md", "CHANGELOG.md"] +} diff --git a/packages/embed/src/client/createGitBook.ts b/packages/embed/src/client/createGitBook.ts new file mode 100644 index 0000000000..1c5b1dc4bd --- /dev/null +++ b/packages/embed/src/client/createGitBook.ts @@ -0,0 +1,14 @@ +export type CreateGitBookOptions = { + /** + * URL of the GitBook site to embed. + */ + siteURL: string; +}; + +export type GitBookClient = {}; + +export function createGitBook(options: CreateGitBookOptions) { + const client: GitBookClient = {}; + + return client; +} diff --git a/packages/embed/src/client/createGitBookFrame.ts b/packages/embed/src/client/createGitBookFrame.ts new file mode 100644 index 0000000000..446b9b9812 --- /dev/null +++ b/packages/embed/src/client/createGitBookFrame.ts @@ -0,0 +1,23 @@ +import { createChannel } from 'bidc'; +import type { CreateGitBookOptions } from './createGitBook'; + +export type GitBookFrameClient = { + /** + * Post a message to the chat. + */ + postUserMessage: (message: string) => void; + + /** + * Register a custom tool. + */ + registerTool: (tool: {}) => void; +}; + +export function createGitBookFrame( + iframe: HTMLIFrameElement, + options: CreateGitBookOptions +): GitBookFrameClient { + const channel = createChannel(iframe.contentWindow); + + // TODO: Implement the client. +} diff --git a/packages/embed/src/client/index.ts b/packages/embed/src/client/index.ts new file mode 100644 index 0000000000..a72f5bde8e --- /dev/null +++ b/packages/embed/src/client/index.ts @@ -0,0 +1 @@ +export * from './createGitBook'; diff --git a/packages/embed/src/index.ts b/packages/embed/src/index.ts new file mode 100644 index 0000000000..4f1cce44fa --- /dev/null +++ b/packages/embed/src/index.ts @@ -0,0 +1 @@ +export * from './client'; diff --git a/packages/embed/src/react/GitBookProvider.tsx b/packages/embed/src/react/GitBookProvider.tsx new file mode 100644 index 0000000000..83c03878ff --- /dev/null +++ b/packages/embed/src/react/GitBookProvider.tsx @@ -0,0 +1,30 @@ +'use client'; + +import * as React from 'react'; +import { type CreateGitBookOptions, createGitBook } from '../client'; +import { GitBookContext } from './context'; + +export function GitBookProvider(props: React.PropsWithChildren) { + const { siteURL, children } = props; + + const options = React.useMemo( + () => ({ + siteURL, + }), + [siteURL] + ); + + const client = React.useMemo(() => createGitBook(options), [options]); + + return {children}; +} + +export function useGitBook() { + const context = React.useContext(GitBookContext); + + if (!context) { + throw new Error('This component must be used within a '); + } + + return context; +} diff --git a/packages/embed/src/react/context.ts b/packages/embed/src/react/context.ts new file mode 100644 index 0000000000..b0c872c59f --- /dev/null +++ b/packages/embed/src/react/context.ts @@ -0,0 +1,6 @@ +'use client'; + +import * as React from 'react'; +import type { GitBookClient } from '../client'; + +export const GitBookContext = React.createContext(null); diff --git a/packages/embed/src/react/index.ts b/packages/embed/src/react/index.ts new file mode 100644 index 0000000000..7b0d5b19cd --- /dev/null +++ b/packages/embed/src/react/index.ts @@ -0,0 +1 @@ +export * from './GitBookProvider'; diff --git a/packages/embed/src/standalone.ts b/packages/embed/src/standalone.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/embed/tsconfig.json b/packages/embed/tsconfig.json new file mode 100644 index 0000000000..37ff331113 --- /dev/null +++ b/packages/embed/tsconfig.json @@ -0,0 +1,22 @@ +{ + "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": [] + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index 76425ea2a3..9064946b8c 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -70,7 +70,7 @@ "usehooks-ts": "^3.1.0", "warn-once": "^0.1.1", "zustand": "^5.0.3", - "bidc": "^0.0.2" + "bidc": "catalog:" }, "devDependencies": { "@argos-ci/playwright": "^5.0.9", From af562532ad8eac90346a2d09d7edccefbbd5568d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Mon, 18 Aug 2025 10:18:20 +0200 Subject: [PATCH 07/48] Continue --- packages/embed/README.md | 4 +- packages/embed/build.ts | 4 + packages/embed/src/client/createGitBook.ts | 51 ++++++++++- .../embed/src/client/createGitBookFrame.ts | 68 ++++++++++++-- packages/embed/src/client/index.ts | 2 + packages/embed/src/client/protocol.ts | 63 +++++++++++++ .../embed/src/react/GitBookAssistantFrame.tsx | 38 ++++++++ packages/embed/src/react/GitBookProvider.tsx | 6 ++ packages/embed/src/standalone.ts | 88 +++++++++++++++++++ 9 files changed, 313 insertions(+), 11 deletions(-) create mode 100644 packages/embed/build.ts create mode 100644 packages/embed/src/client/protocol.ts create mode 100644 packages/embed/src/react/GitBookAssistantFrame.tsx diff --git a/packages/embed/README.md b/packages/embed/README.md index 64cabbaf0c..e2c67a416b 100644 --- a/packages/embed/README.md +++ b/packages/embed/README.md @@ -32,9 +32,9 @@ import { GitBook } from '@gitbook/embed'; After installing the NPM package, you can import prebuilt React components: ```tsx -import { GitBookProvider, GitBookChatView } from '@gitbook/embed/react'; +import { GitBookProvider, GitBookAssistantFrame } from '@gitbook/embed/react'; - + ``` diff --git a/packages/embed/build.ts b/packages/embed/build.ts new file mode 100644 index 0000000000..896fb8cc6c --- /dev/null +++ b/packages/embed/build.ts @@ -0,0 +1,4 @@ +await Bun.build({ + entrypoints: ['./index.tsx'], + outdir: './build', +}); diff --git a/packages/embed/src/client/createGitBook.ts b/packages/embed/src/client/createGitBook.ts index 1c5b1dc4bd..bcd57a0961 100644 --- a/packages/embed/src/client/createGitBook.ts +++ b/packages/embed/src/client/createGitBook.ts @@ -1,3 +1,5 @@ +import { type GitBookFrameClient, createGitBookFrame } from './createGitBookFrame'; + export type CreateGitBookOptions = { /** * URL of the GitBook site to embed. @@ -5,10 +7,55 @@ export type CreateGitBookOptions = { siteURL: string; }; -export type GitBookClient = {}; +export type GetFrameURLOptions = { + /** + * Authentication to use for the frame. + */ + visitor?: { + /** + * Signed JWT token for Adaptive Content or Visitor Authentication to use. + */ + token?: string; + + /** + * Unsigned claims to pass to the frame. + * You can use these claims in dynamic expressions using `visitor.claims.unsigned.`. + */ + unsignedClaims?: Record; + }; +}; + +export type GitBookClient = { + /** + * Get the URL for a GitBook frame. + */ + getFrameURL: (options: GetFrameURLOptions) => string; + /** + * Create a new GitBook frame. + */ + createFrame: (iframe: HTMLIFrameElement) => GitBookFrameClient; +}; export function createGitBook(options: CreateGitBookOptions) { - const client: GitBookClient = {}; + const client: GitBookClient = { + getFrameURL: (frameOptions) => { + const url = new URL(options.siteURL); + url.pathname = `${url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`}~gitbook/embed/assistant`; + + if (frameOptions.visitor?.token) { + url.searchParams.set('token', frameOptions.visitor.token); + } + + if (frameOptions.visitor?.unsignedClaims) { + Object.entries(frameOptions.visitor.unsignedClaims).forEach(([key, value]) => { + url.searchParams.set(`visitor.${key}`, String(value)); + }); + } + + return url.toString(); + }, + createFrame: (iframe) => createGitBookFrame(iframe), + }; return client; } diff --git a/packages/embed/src/client/createGitBookFrame.ts b/packages/embed/src/client/createGitBookFrame.ts index 446b9b9812..f9788c7162 100644 --- a/packages/embed/src/client/createGitBookFrame.ts +++ b/packages/embed/src/client/createGitBookFrame.ts @@ -1,5 +1,10 @@ import { createChannel } from 'bidc'; -import type { CreateGitBookOptions } from './createGitBook'; +import type { + FrameToParentMessage, + GitBookPlaceholderSettings, + GitBookToolDefinition, + ParentToFrameMessage, +} from './protocol'; export type GitBookFrameClient = { /** @@ -10,14 +15,63 @@ export type GitBookFrameClient = { /** * Register a custom tool. */ - registerTool: (tool: {}) => void; + registerTool: (tool: GitBookToolDefinition) => void; + + /** + * Clear the chat. + */ + clearChat: () => void; + + /** + * Set the placeholder settings. + */ + setPlaceholder: (placeholder: GitBookPlaceholderSettings) => void; + + /** + * Register an event listener. + */ + on: (event: string, listener: (...args: any[]) => void) => () => void; }; -export function createGitBookFrame( - iframe: HTMLIFrameElement, - options: CreateGitBookOptions -): GitBookFrameClient { +/** + * Create a client to communicate with the GitBook Assistant frame. + */ +export function createGitBookFrame(iframe: HTMLIFrameElement): GitBookFrameClient { + if (!iframe.contentWindow) { + throw new Error('Iframe must have a content window'); + } const channel = createChannel(iframe.contentWindow); - // TODO: Implement the client. + channel.receive((message: FrameToParentMessage) => { + if (message.type === 'close') { + const listeners = events.get('close') || []; + if (listeners) { + listeners.forEach((listener) => listener()); + } + } + }); + + const sendToFrame = (message: ParentToFrameMessage) => { + channel.send(message); + }; + + const events = new Map void>>(); + + return { + postUserMessage: (message) => sendToFrame({ type: 'postUserMessage', message }), + registerTool: (tool) => sendToFrame({ type: 'registerTool', tool }), + clearChat: () => sendToFrame({ type: 'clearChat' }), + setPlaceholder: (settings) => sendToFrame({ type: 'setPlaceholder', settings }), + on: (event, listener) => { + const listeners = events.get(event) || []; + listeners.push(listener); + events.set(event, listeners); + return () => { + events.set( + event, + listeners.filter((l) => l !== listener) + ); + }; + }, + }; } diff --git a/packages/embed/src/client/index.ts b/packages/embed/src/client/index.ts index a72f5bde8e..27098d5b3a 100644 --- a/packages/embed/src/client/index.ts +++ b/packages/embed/src/client/index.ts @@ -1 +1,3 @@ export * from './createGitBook'; +export * from './createGitBookFrame'; +export * from './protocol'; diff --git a/packages/embed/src/client/protocol.ts b/packages/embed/src/client/protocol.ts new file mode 100644 index 0000000000..9a23f3c4b5 --- /dev/null +++ b/packages/embed/src/client/protocol.ts @@ -0,0 +1,63 @@ +import type { AIToolCallResult, AIToolDefinition } from '@gitbook/api'; +import type { IconName } from '@gitbook/icons'; + +/** + * Custom tool definition to be passed to the AI assistant. + */ +export type GitBookToolDefinition = 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>; +}; + +/** + * Placeholder settings. + */ +export type GitBookPlaceholderSettings = { + /** + * Welcome message to be displayed in the placeholder. + */ + welcomeMessage: string; + + /** + * Suggestions to be displayed in the placeholder. + */ + suggestions: string[]; +}; + +/** + * Messages sent from the parent to the frame. + */ +export type ParentToFrameMessage = + | { + type: 'postUserMessage'; + message: string; + } + | { + type: 'registerTool'; + tool: GitBookToolDefinition; + } + | { + type: 'clearChat'; + } + | { + type: 'setPlaceholder'; + settings: GitBookPlaceholderSettings; + }; + +/** + * Messages sent from the frame to the parent. + */ +export type FrameToParentMessage = { + type: 'close'; +}; diff --git a/packages/embed/src/react/GitBookAssistantFrame.tsx b/packages/embed/src/react/GitBookAssistantFrame.tsx new file mode 100644 index 0000000000..a6e4980f07 --- /dev/null +++ b/packages/embed/src/react/GitBookAssistantFrame.tsx @@ -0,0 +1,38 @@ +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(null); + const gitbookFrameRef = React.useRef(null); + const gitbook = useGitBook(); + const frameURL = gitbook.getFrameURL(frameOptions); + + React.useEffect(() => { + if (frameRef.current) { + gitbookFrameRef.current = gitbook.createFrame(frameRef.current); + } + }, [gitbook]); + + return ( +
+