mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-17 16:15:22 +00:00
Update Docs Embed with new styling and tabs (#3823)
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@gitbook/embed": minor
|
||||||
|
"gitbook": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Improve Docs Embed with separate Assistant and Docs tabs
|
||||||
+370
-8
@@ -1,24 +1,61 @@
|
|||||||
# `@gitbook/embed`
|
# GitBook Docs Embed (`@gitbook/embed`)
|
||||||
|
|
||||||
Embed the GitBook Docs Assistant in your product or website.
|
Embed your GitBook docs in your product or website.
|
||||||
|
|
||||||
|
The Docs Embed can contain two tabs:
|
||||||
|
- **Assistant**: The [GitBook Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant) - an AI-powered chat interface to help users find answers
|
||||||
|
- **Docs**: A browser for navigating your documentation site
|
||||||
|
|
||||||
|
The embed is set up automatically based on your site's configuration. You can optionally customize and override the configuration with custom actions, tools, suggested questions, [Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access), and more. See the [Configuration](#configuration) section for all available options.
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
## As a script from your docs site
|
## As a standalone script from your docs site
|
||||||
|
|
||||||
All GitBook docs site includes a script to easily embed the docs assistant as a widget on your website.
|
All GitBook docs sites include a script to easily add the Docs Embed as a widget on your website.
|
||||||
|
|
||||||
The script is served at `https://docs.company.com/~gitbook/embed/script.js`.
|
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.
|
You can find the embed script from your docs site settings, or you can copy the following and replace `docs.company.com` with your docs site hostname.
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script src="https://docs.company.com/~gitbook/embed/script.js"></script>
|
<script src="https://docs.company.com/~gitbook/embed/script.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
// Initialize with Authenticated Access (optional)
|
||||||
|
window.GitBook('init',
|
||||||
|
{ siteURL: 'https://docs.company.com' },
|
||||||
|
{ visitor: { token: 'your-jwt-token' } }
|
||||||
|
);
|
||||||
window.GitBook('show');
|
window.GitBook('show');
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The standalone script provides a global `GitBook` function. See the [API Reference](#api-reference) section for all available methods.
|
||||||
|
|
||||||
|
### Example: Configuring the widget
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
GitBook('configure', {
|
||||||
|
button: {
|
||||||
|
label: 'Ask',
|
||||||
|
icon: 'assistant' // 'assistant' | 'sparkle' | 'help' | 'book'
|
||||||
|
},
|
||||||
|
tabs: ['assistant', 'docs'],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: 'circle-question',
|
||||||
|
label: 'Contact Support',
|
||||||
|
onClick: () => window.open('https://support.example.com', '_blank')
|
||||||
|
}
|
||||||
|
],
|
||||||
|
greeting: { title: 'Welcome!', subtitle: 'How can I help?' },
|
||||||
|
suggestions: ['What is GitBook?', 'How do I get started?'],
|
||||||
|
tools: [/* ... */]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [Configuration](#configuration) section for all available options.
|
||||||
|
|
||||||
## As a package from NPM
|
## As a package from NPM
|
||||||
|
|
||||||
Install the package: `npm install @gitbook/embed` and import it in your web application:
|
Install the package: `npm install @gitbook/embed` and import it in your web application:
|
||||||
@@ -30,10 +67,46 @@ const gitbook = createGitBook({
|
|||||||
siteURL: 'https://docs.company.com'
|
siteURL: 'https://docs.company.com'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create an iframe and get its URL
|
||||||
const iframe = document.createElement('iframe');
|
const iframe = document.createElement('iframe');
|
||||||
iframe.src = gitbook.getFrameURL();
|
iframe.src = gitbook.getFrameURL({
|
||||||
|
visitor: {
|
||||||
|
token: 'your-jwt-token', // Optional: for Adaptive Content or Authenticated Access
|
||||||
|
unsignedClaims: { // Optional: custom claims for dynamic expressions
|
||||||
|
userId: '123',
|
||||||
|
plan: 'premium'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a frame client to communicate with the iframe
|
||||||
const frame = gitbook.createFrame(iframe);
|
const frame = gitbook.createFrame(iframe);
|
||||||
|
|
||||||
|
// Use the frame client methods
|
||||||
|
frame.navigateToPage('/getting-started'); // Navigate to a page in the docs tab
|
||||||
|
frame.navigateToAssistant(); // Switch to the assistant tab
|
||||||
|
frame.postUserMessage('How do I get started?');
|
||||||
|
frame.clearChat();
|
||||||
|
|
||||||
|
// Configure the embed (see Configuration section for all options)
|
||||||
|
frame.configure({
|
||||||
|
tabs: ['assistant', 'docs'],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: 'circle-question',
|
||||||
|
label: 'Contact Support',
|
||||||
|
onClick: () => window.open('https://support.example.com', '_blank')
|
||||||
|
}
|
||||||
|
],
|
||||||
|
greeting: { title: 'Welcome!', subtitle: 'How can I help?' },
|
||||||
|
suggestions: ['What is GitBook?', 'How do I get started?'],
|
||||||
|
tools: [/* ... */]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen to events
|
||||||
|
frame.on('close', () => {
|
||||||
|
console.log('Frame closed');
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## As React components
|
## As React components
|
||||||
@@ -41,9 +114,298 @@ const frame = gitbook.createFrame(iframe);
|
|||||||
After installing the NPM package, you can import prebuilt React components:
|
After installing the NPM package, you can import prebuilt React components:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { GitBookProvider, GitBookAssistantFrame } from '@gitbook/embed/react';
|
import { GitBookProvider, GitBookFrame } from '@gitbook/embed/react';
|
||||||
|
|
||||||
<GitBookProvider siteURL="https://docs.company.com">
|
<GitBookProvider siteURL="https://docs.company.com">
|
||||||
<GitBookAssistantFrame />
|
<GitBookFrame
|
||||||
|
visitor={{
|
||||||
|
token: 'your-jwt-token', // Optional: for Adaptive Content or Authenticated Access
|
||||||
|
unsignedClaims: { userId: '123' } // Optional: custom claims for dynamic expressions
|
||||||
|
}}
|
||||||
|
tabs={['assistant', 'docs']}
|
||||||
|
greeting={{ title: 'Welcome!', subtitle: 'How can I help?' }}
|
||||||
|
suggestions={['What is GitBook?', 'How do I get started?']}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
icon: 'circle-question',
|
||||||
|
label: 'Contact Support',
|
||||||
|
onClick: () => window.open('https://support.example.com', '_blank')
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
tools={[/* ... */]}
|
||||||
|
/>
|
||||||
</GitBookProvider>
|
</GitBookProvider>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can also use the `useGitBook` hook to access the client:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useGitBook } from '@gitbook/embed/react';
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const gitbook = useGitBook();
|
||||||
|
const frameURL = gitbook.getFrameURL({ visitor: { token: '...' } });
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# API Reference
|
||||||
|
|
||||||
|
## Method Comparison
|
||||||
|
|
||||||
|
| Method | Standalone Script | NPM Package | React Components |
|
||||||
|
|--------|------------------|-------------|------------------|
|
||||||
|
| **Initialize** | `GitBook('init', options, frameOptions)` | `createGitBook(options)` | `<GitBookProvider siteURL="...">` |
|
||||||
|
| **Get frame URL** | ❌ (handled internally) | `client.getFrameURL(options)` | `useGitBook().getFrameURL(options)` |
|
||||||
|
| **Create frame client** | ❌ (handled internally) | `client.createFrame(iframe)` | `useGitBook().createFrame(iframe)` |
|
||||||
|
| **Show/Hide widget** | `GitBook('show')` / `GitBook('hide')` | ❌ | ❌ |
|
||||||
|
| **Open/Close window** | `GitBook('open')` / `GitBook('close')` / `GitBook('toggle')` | ❌ | ❌ |
|
||||||
|
| **Navigate to page** | `GitBook('navigateToPage', path)` | `frame.navigateToPage(path)` | Via frame client |
|
||||||
|
| **Navigate to assistant** | `GitBook('navigateToAssistant')` | `frame.navigateToAssistant()` | Via frame client |
|
||||||
|
| **Post message** | `GitBook('postUserMessage', message)` | `frame.postUserMessage(message)` | Via frame client |
|
||||||
|
| **Clear chat** | `GitBook('clearChat')` | `frame.clearChat()` | Via frame client |
|
||||||
|
| **Configure** | `GitBook('configure', settings)` | `frame.configure(settings)` | Props on `<GitBookFrame>` |
|
||||||
|
| **Event listeners** | ❌ | `frame.on(event, listener)` | Via frame client |
|
||||||
|
| **Unload** | `GitBook('unload')` | ❌ | ❌ |
|
||||||
|
|
||||||
|
## Method Signatures
|
||||||
|
|
||||||
|
### Standalone Script
|
||||||
|
|
||||||
|
- `GitBook('init', options: { siteURL: string }, frameOptions?: { visitor?: {...} })` - Initialize widget
|
||||||
|
- `GitBook('show')` - Show widget button
|
||||||
|
- `GitBook('hide')` - Hide widget button
|
||||||
|
- `GitBook('open')` - Open widget window
|
||||||
|
- `GitBook('close')` - Close widget window
|
||||||
|
- `GitBook('toggle')` - Toggle widget window
|
||||||
|
- `GitBook('navigateToPage', path: string)` - Navigate to page
|
||||||
|
- `GitBook('navigateToAssistant')` - Navigate to assistant tab
|
||||||
|
- `GitBook('postUserMessage', message: string)` - Post message to chat
|
||||||
|
- `GitBook('clearChat')` - Clear chat history
|
||||||
|
- `GitBook('configure', settings: {...})` - Configure widget
|
||||||
|
- `GitBook('unload')` - Unload widget
|
||||||
|
|
||||||
|
### NPM Package
|
||||||
|
|
||||||
|
**Client Factory:**
|
||||||
|
- `createGitBook(options: { siteURL: string })` → `GitBookClient`
|
||||||
|
- `client.getFrameURL(options?: { visitor?: {...} })` → `string`
|
||||||
|
- `client.createFrame(iframe: HTMLIFrameElement)` → `GitBookFrameClient`
|
||||||
|
|
||||||
|
**Frame Client:**
|
||||||
|
- `frame.navigateToPage(path: string)` → `void`
|
||||||
|
- `frame.navigateToAssistant()` → `void`
|
||||||
|
- `frame.postUserMessage(message: string)` → `void`
|
||||||
|
- `frame.clearChat()` → `void`
|
||||||
|
- `frame.configure(settings: Partial<GitBookEmbeddableConfiguration>)` → `void`
|
||||||
|
- `frame.on(event: string, listener: Function)` → `() => void` (unsubscribe)
|
||||||
|
|
||||||
|
### React Components
|
||||||
|
|
||||||
|
**Components:**
|
||||||
|
- `<GitBookProvider siteURL: string>` - Provider component
|
||||||
|
- `<GitBookFrame {...props}>` - Frame component (accepts all config options as props)
|
||||||
|
|
||||||
|
**Hooks:**
|
||||||
|
- `useGitBook()` → `GitBookClient` (must be used within `<GitBookProvider>`)
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
|
||||||
|
Configuration options are available across usage methods as follows:
|
||||||
|
- **Standalone script**: via `GitBook('configure', {...})`
|
||||||
|
- **NPM package**: via `frame.configure({...})`
|
||||||
|
- **React components**: via props on `<GitBookFrame>`
|
||||||
|
|
||||||
|
### `tabs`
|
||||||
|
|
||||||
|
Available in: Standalone script, NPM package, React components
|
||||||
|
|
||||||
|
Override which tabs are displayed. Defaults to your site's configuration.
|
||||||
|
|
||||||
|
- **Type**: `('assistant' | 'docs')[]`
|
||||||
|
- **Options**:
|
||||||
|
- `['assistant', 'docs']` - Show both tabs
|
||||||
|
- `['assistant']` - Show only the assistant tab
|
||||||
|
- `['docs']` - Show only the docs tab
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
tabs: ['assistant', 'docs']
|
||||||
|
```
|
||||||
|
|
||||||
|
### `actions`
|
||||||
|
|
||||||
|
Available in: Standalone script, NPM package, React components
|
||||||
|
|
||||||
|
Custom action buttons rendered in the sidebar alongside tabs. Each action button triggers a callback when clicked.
|
||||||
|
|
||||||
|
**Note**: This prop was previously named `buttons`. Use `actions` instead, it has the same functionality.
|
||||||
|
|
||||||
|
- **Type**: `GitBookEmbeddableActionDefinition[]`
|
||||||
|
- **Properties**:
|
||||||
|
- `icon`: `string` - Icon name. Any [FontAwesome icon](https://fontawesome.com/search) is supported. (e.g., `'rocket'`, `'comments'`, `'user-circle'`, ...)
|
||||||
|
- `label`: `string` - Button label text
|
||||||
|
- `onClick`: `() => void | Promise<void>` - Callback function when clicked
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: 'comments',
|
||||||
|
label: 'Contact Support',
|
||||||
|
onClick: () => window.open('https://support.example.com', '_blank')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'rocket',
|
||||||
|
label: 'Get started',
|
||||||
|
onClick: () => {
|
||||||
|
GitBook('navigateToPage', '/getting-started');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### `greeting`
|
||||||
|
|
||||||
|
Available in: Standalone script, NPM package, React components
|
||||||
|
|
||||||
|
Welcome message displayed in the [Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant) tab.
|
||||||
|
|
||||||
|
- **Type**: `{ title: string, subtitle: string }`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
greeting: {
|
||||||
|
title: 'Welcome!',
|
||||||
|
subtitle: 'How can I help you today?'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `suggestions`
|
||||||
|
|
||||||
|
Available in: Standalone script, NPM package, React components
|
||||||
|
|
||||||
|
Suggested questions displayed in the [Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant) welcome screen.
|
||||||
|
|
||||||
|
- **Type**: `string[]`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
suggestions: [
|
||||||
|
'What is GitBook?',
|
||||||
|
'How do I get started?',
|
||||||
|
'What can you do?'
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### `tools`
|
||||||
|
|
||||||
|
Available in: Standalone script, NPM package, React components
|
||||||
|
|
||||||
|
Custom AI tools to extend the [Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant). Tools allow the assistant to execute functions and integrate with your own systems.
|
||||||
|
|
||||||
|
**Note**: In addition to custom tools, the Assistant will always have access to any [MCP servers you define](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant#extend-gitbook-assistant-with-mcp-servers) in your site's AI settings.
|
||||||
|
|
||||||
|
- **Type**: `GitBookToolDefinition[]`
|
||||||
|
- **Properties**:
|
||||||
|
- `name`: `string` - Unique tool identifier
|
||||||
|
- `description`: `string` - Description of what the tool does (used by the AI to decide when and how to use it).
|
||||||
|
- `inputSchema`: `object` - JSON schema defining the tool's input parameters
|
||||||
|
- `execute`: `(input: object) => Promise<{ output: any, summary: string }>` - Async function that executes the tool.
|
||||||
|
- `output`: The result of the tool execution, provided to the AI to continue working with. Not shown to the user.
|
||||||
|
- `summary`: The visual summary of the tool execution, shown in the user's chat window.
|
||||||
|
- `confirmation`: `{ icon?: string, label: string }` (optional) - Confirmation button shown before execution, useful for actions that require the user's express approval.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
name: 'get_user_info',
|
||||||
|
description: 'Get information about the current user',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
userId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The user ID to look up'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['userId']
|
||||||
|
},
|
||||||
|
execute: async (input) => {
|
||||||
|
const user = await fetch(`/api/users/${input.userId}`).then(r => r.json());
|
||||||
|
return {
|
||||||
|
output: { name: user.name, plan: user.plan },
|
||||||
|
summary: `Retrieved info for user ${user.name}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'create_ticket',
|
||||||
|
description: 'Create a support ticket',
|
||||||
|
confirmation: {
|
||||||
|
icon: 'circle-question',
|
||||||
|
label: 'Create support ticket?'
|
||||||
|
},
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
subject: { type: 'string' },
|
||||||
|
description: { type: 'string' }
|
||||||
|
},
|
||||||
|
required: ['subject', 'description']
|
||||||
|
},
|
||||||
|
execute: async (input) => {
|
||||||
|
const ticket = await fetch('/api/tickets', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(input)
|
||||||
|
}).then(r => r.json());
|
||||||
|
return {
|
||||||
|
output: { ticketId: ticket.id },
|
||||||
|
summary: `Created ticket #${ticket.id}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### `visitor` (Authenticated Access)
|
||||||
|
|
||||||
|
Available in: Standalone script (via `init`), NPM package (via `getFrameURL()`), React components (as prop)
|
||||||
|
|
||||||
|
[Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access) options passed when creating the frame URL. Used for [Adaptive Content](https://gitbook.com/docs/publishing-documentation/adaptive-content) and [Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access).
|
||||||
|
|
||||||
|
**Note**: This is not a configuration option but rather a parameter when initializing the frame or creating the frame URL.
|
||||||
|
|
||||||
|
**Standalone script**: Pass as the second argument to `GitBook('init', options, frameOptions)`
|
||||||
|
**NPM package**: Pass to `getFrameURL({ visitor: {...} })`
|
||||||
|
**React components**: Pass as the `visitor` prop on `<GitBookFrame>`
|
||||||
|
|
||||||
|
- **Type**: `{ token?: string, unsignedClaims?: Record<string, unknown> }`
|
||||||
|
- **Properties**:
|
||||||
|
- `token`: `string` (optional) - Signed JWT token for [Adaptive Content](https://gitbook.com/docs/publishing-documentation/adaptive-content) or [Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access)
|
||||||
|
- `unsignedClaims`: `Record<string, unknown>` (optional) - Unsigned claims that can be used in dynamic expressions via `visitor.claims.unsigned.<claim-name>`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
visitor: {
|
||||||
|
token: 'your-jwt-token',
|
||||||
|
unsignedClaims: {
|
||||||
|
userId: '123',
|
||||||
|
plan: 'premium',
|
||||||
|
role: 'admin'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `button`
|
||||||
|
|
||||||
|
Available in: Standalone script only
|
||||||
|
|
||||||
|
Configure the widget button for the standalone script. This option is not available when using the NPM package or React components, since they can be customized completely.
|
||||||
|
|
||||||
|
- **Type**: `{ label: string, icon: 'assistant' | 'sparkle' | 'help' | 'book' }`
|
||||||
|
- **Properties**:
|
||||||
|
- `label`: `string` - Button label text
|
||||||
|
- `icon`: `'assistant' | 'sparkle' | 'help' | 'book'` - Icon displayed on the button. Choose from one of 4 presets.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
button: {
|
||||||
|
label: 'Ask',
|
||||||
|
icon: 'assistant'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function createGitBook(options: CreateGitBookOptions) {
|
|||||||
const client: GitBookClient = {
|
const client: GitBookClient = {
|
||||||
getFrameURL: (frameOptions) => {
|
getFrameURL: (frameOptions) => {
|
||||||
const url = new URL(options.siteURL);
|
const url = new URL(options.siteURL);
|
||||||
url.pathname = `${url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`}~gitbook/embed/assistant`;
|
url.pathname = `${url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`}~gitbook/embed`;
|
||||||
|
|
||||||
if (frameOptions.visitor?.token) {
|
if (frameOptions.visitor?.token) {
|
||||||
url.searchParams.set('token', frameOptions.visitor.token);
|
url.searchParams.set('token', frameOptions.visitor.token);
|
||||||
|
|||||||
@@ -64,8 +64,9 @@ export function createGitBookFrame(iframe: HTMLIFrameElement): GitBookFrameClien
|
|||||||
const events = new Map<string, Array<(...args: any[]) => void>>();
|
const events = new Map<string, Array<(...args: any[]) => void>>();
|
||||||
|
|
||||||
const configuration: GitBookEmbeddableConfiguration = {
|
const configuration: GitBookEmbeddableConfiguration = {
|
||||||
buttons: [],
|
tabs: ['assistant', 'docs'],
|
||||||
welcomeMessage: '',
|
actions: [],
|
||||||
|
greeting: { title: '', subtitle: '' },
|
||||||
suggestions: [],
|
suggestions: [],
|
||||||
tools: [],
|
tools: [],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export type GitBookToolDefinition = AIToolDefinition & {
|
|||||||
/**
|
/**
|
||||||
* Custom button definition to be passed to the embeddable GitBook.
|
* Custom button definition to be passed to the embeddable GitBook.
|
||||||
*/
|
*/
|
||||||
export type GitBookEmbeddableButtonDefinition = {
|
export type GitBookEmbeddableActionDefinition = {
|
||||||
/**
|
/**
|
||||||
* Icon to be displayed in the button.
|
* Icon to be displayed in the button.
|
||||||
*/
|
*/
|
||||||
@@ -41,16 +41,26 @@ export type GitBookEmbeddableButtonDefinition = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overall configuration for the layout of the embeddable GitBook.
|
* Overall configuration for the layout of the GitBook embed.
|
||||||
*/
|
*/
|
||||||
export type GitBookEmbeddableConfiguration = {
|
export type GitBookEmbeddableConfiguration = {
|
||||||
|
/** Tabs to display in the embed (if enabled on the site). */
|
||||||
|
tabs: ('assistant' | 'docs')[];
|
||||||
|
|
||||||
|
/** Additional buttons to be displayed in the header of the GitBook embed. */
|
||||||
|
actions: GitBookEmbeddableActionDefinition[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Buttons to be displayed in the header of the embeddable GitBook.
|
* Additional buttons to be displayed in the header of the GitBook embed.
|
||||||
|
* @deprecated Use `actions` instead.
|
||||||
*/
|
*/
|
||||||
buttons: GitBookEmbeddableButtonDefinition[];
|
buttons?: GitBookEmbeddableActionDefinition[];
|
||||||
|
|
||||||
/** Message to be displayed in the welcome page. */
|
/** Message to be displayed in the welcome page. */
|
||||||
welcomeMessage: string;
|
greeting: {
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
};
|
||||||
|
|
||||||
/** Suggestions of questions to be displayed in the welcome page. */
|
/** Suggestions of questions to be displayed in the welcome page. */
|
||||||
suggestions: string[];
|
suggestions: string[];
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export type GitBookFrameProps = {
|
|||||||
* Render a frame with the GitBook Assistant in it.
|
* Render a frame with the GitBook Assistant in it.
|
||||||
*/
|
*/
|
||||||
export function GitBookFrame(props: GitBookFrameProps) {
|
export function GitBookFrame(props: GitBookFrameProps) {
|
||||||
const { className, visitor, buttons, welcomeMessage, suggestions, tools } = props;
|
const { className, visitor, actions, greeting, suggestions, tools } = props;
|
||||||
|
|
||||||
const frameRef = useRef<HTMLIFrameElement>(null);
|
const frameRef = useRef<HTMLIFrameElement>(null);
|
||||||
const gitbook = useGitBook();
|
const gitbook = useGitBook();
|
||||||
@@ -33,12 +33,13 @@ export function GitBookFrame(props: GitBookFrameProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
gitbookFrame?.configure({
|
gitbookFrame?.configure({
|
||||||
buttons,
|
tabs: ['assistant', 'docs'],
|
||||||
welcomeMessage,
|
actions,
|
||||||
|
greeting,
|
||||||
suggestions,
|
suggestions,
|
||||||
tools,
|
tools,
|
||||||
});
|
});
|
||||||
}, [gitbookFrame, buttons, welcomeMessage, suggestions, tools]);
|
}, [gitbookFrame, actions, greeting, suggestions, tools]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<iframe
|
<iframe
|
||||||
|
|||||||
@@ -31,24 +31,50 @@ type StandaloneCalls =
|
|||||||
// Clear the chat
|
// Clear the chat
|
||||||
| ['clearChat']
|
| ['clearChat']
|
||||||
// Configure the embed
|
// Configure the embed
|
||||||
| ['configure', Partial<GitBookEmbeddableConfiguration>]
|
| ['configure', Partial<GitBookEmbeddableConfiguration & StandaloneConfiguration>]
|
||||||
// Navigate to a page
|
// Navigate to a page
|
||||||
| ['navigateToPage', string]
|
| ['navigateToPage', string]
|
||||||
// Navigate to the assistant
|
// Navigate to the assistant
|
||||||
| ['navigateToAssistant'];
|
| ['navigateToAssistant'];
|
||||||
|
|
||||||
|
type StandaloneConfiguration = {
|
||||||
|
/** Configure the button to open the embed */
|
||||||
|
button: {
|
||||||
|
/** Label to be displayed in the button. */
|
||||||
|
label: string;
|
||||||
|
/** Icon to be displayed in the button. */
|
||||||
|
icon: 'assistant' | 'sparkle' | 'help' | 'book';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type GitBookStandalone = ((...args: StandaloneCalls) => void) & {
|
export type GitBookStandalone = ((...args: StandaloneCalls) => void) & {
|
||||||
q?: StandaloneCalls[];
|
q?: StandaloneCalls[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let widgetIframe: HTMLIFrameElement | undefined;
|
||||||
|
let _client: GitBookClient | undefined;
|
||||||
|
let _frame: GitBookFrameClient | undefined;
|
||||||
|
let frameOptions: GetFrameURLOptions | undefined;
|
||||||
|
let frameConfiguration: GitBookEmbeddableConfiguration & StandaloneConfiguration = {
|
||||||
|
button: {
|
||||||
|
label: 'Ask',
|
||||||
|
icon: 'assistant',
|
||||||
|
},
|
||||||
|
actions: [],
|
||||||
|
greeting: { title: '', subtitle: '' },
|
||||||
|
suggestions: [],
|
||||||
|
tools: [],
|
||||||
|
tabs: ['assistant', 'docs'],
|
||||||
|
};
|
||||||
|
|
||||||
const widgetButton = document.createElement('button');
|
const widgetButton = document.createElement('button');
|
||||||
widgetButton.id = 'gitbook-widget-button';
|
widgetButton.id = 'gitbook-widget-button';
|
||||||
widgetButton.addEventListener('click', () => {
|
widgetButton.addEventListener('click', () => {
|
||||||
GitBook('toggle');
|
GitBook('toggle');
|
||||||
});
|
});
|
||||||
widgetButton.innerHTML = `
|
widgetButton.innerHTML = `
|
||||||
<span id="gitbook-widget-button-icon"></span>
|
<span id="gitbook-widget-button-icon" data-icon="${frameConfiguration.button.icon}"></span>
|
||||||
<span id="gitbook-widget-button-label">Ask</span>
|
<span id="gitbook-widget-button-label">${frameConfiguration.button.label}</span>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const widgetWindow = document.createElement('div');
|
const widgetWindow = document.createElement('div');
|
||||||
@@ -58,17 +84,6 @@ widgetWindow.classList.add('hidden');
|
|||||||
document.body.appendChild(widgetButton);
|
document.body.appendChild(widgetButton);
|
||||||
document.body.appendChild(widgetWindow);
|
document.body.appendChild(widgetWindow);
|
||||||
|
|
||||||
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() {
|
function getClient() {
|
||||||
if (!_client) {
|
if (!_client) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -135,27 +150,31 @@ const GitBook = (...args: StandaloneCalls) => {
|
|||||||
case 'postUserMessage':
|
case 'postUserMessage':
|
||||||
getIframe().frame.postUserMessage(args[1]);
|
getIframe().frame.postUserMessage(args[1]);
|
||||||
break;
|
break;
|
||||||
case 'configure':
|
case 'configure': {
|
||||||
|
const settings = args[1];
|
||||||
frameConfiguration = {
|
frameConfiguration = {
|
||||||
...frameConfiguration,
|
...frameConfiguration,
|
||||||
...args[1],
|
...settings,
|
||||||
};
|
};
|
||||||
|
// Update the button label and icon
|
||||||
|
if (settings.button?.label) {
|
||||||
|
const label = widgetButton.querySelector('#gitbook-widget-button-label');
|
||||||
|
if (label) {
|
||||||
|
label.textContent = settings.button.label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (settings.button?.icon) {
|
||||||
|
const icon = widgetButton.querySelector('#gitbook-widget-button-icon');
|
||||||
|
if (icon) {
|
||||||
|
icon.setAttribute('data-icon', settings.button.icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getIframe().frame.configure({
|
getIframe().frame.configure({
|
||||||
...frameConfiguration,
|
...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;
|
||||||
|
|||||||
@@ -113,6 +113,18 @@
|
|||||||
background-color: currentColor;
|
background-color: currentColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#gitbook-widget-button-icon[data-icon="sparkle"] {
|
||||||
|
mask-image: url("https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/sparkle.svg?v=2&token=a463935e93");
|
||||||
|
}
|
||||||
|
|
||||||
|
#gitbook-widget-button-icon[data-icon="help"] {
|
||||||
|
mask-image: url("https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/circle-question.svg?v=2&token=a463935e93");
|
||||||
|
}
|
||||||
|
|
||||||
|
#gitbook-widget-button-icon[data-icon="book"] {
|
||||||
|
mask-image: url("https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/book-open.svg?v=2&token=a463935e93");
|
||||||
|
}
|
||||||
|
|
||||||
#gitbook-widget-button.open #gitbook-widget-button-icon {
|
#gitbook-widget-button.open #gitbook-widget-button-icon {
|
||||||
mask-image: url('https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/close.svg?v=2&token=a463935e93');
|
mask-image: url('https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/close.svg?v=2&token=a463935e93');
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-2
@@ -1,7 +1,28 @@
|
|||||||
|
import type { RouteLayoutParams } from '@/app/utils';
|
||||||
import { EmbeddableAssistantPage } from '@/components/Embeddable';
|
import { EmbeddableAssistantPage } from '@/components/Embeddable';
|
||||||
|
import { getEmbeddableDynamicContext } from '@/lib/embeddable';
|
||||||
|
import { CustomizationAIMode } from '@gitbook/api';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
type PageProps = {
|
||||||
|
params: Promise<RouteLayoutParams>;
|
||||||
|
};
|
||||||
|
|
||||||
export const dynamic = 'force-static';
|
export const dynamic = 'force-static';
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page(props: PageProps) {
|
||||||
return <EmbeddableAssistantPage />;
|
const params = await props.params;
|
||||||
|
const { context } = await getEmbeddableDynamicContext(params);
|
||||||
|
|
||||||
|
// If the assistant is not enabled, redirect to the docs
|
||||||
|
if (context.customization.ai.mode !== CustomizationAIMode.Assistant) {
|
||||||
|
redirect(`${context.linker.toPathInSite('~gitbook/embed/page/')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EmbeddableAssistantPage
|
||||||
|
baseURL={context.linker.toPathInSite('~gitbook/embed/')}
|
||||||
|
siteTitle={context.site.title}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
import type { RouteLayoutParams } from '@/app/utils';
|
||||||
|
import { getEmbeddableDynamicContext } from '@/lib/embeddable';
|
||||||
|
import { CustomizationAIMode } from '@gitbook/api';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
type PageProps = {
|
||||||
|
params: Promise<RouteLayoutParams>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Page(props: PageProps) {
|
||||||
|
const params = await props.params;
|
||||||
|
const { context } = await getEmbeddableDynamicContext(params);
|
||||||
|
const baseURL = context.linker.toPathInSite('~gitbook/embed/');
|
||||||
|
|
||||||
|
// If assistant is enabled, redirect to assistant, otherwise to docs
|
||||||
|
if (context.customization.ai.mode === CustomizationAIMode.Assistant) {
|
||||||
|
redirect(`${baseURL}/assistant`);
|
||||||
|
} else {
|
||||||
|
redirect(`${baseURL}/page/`);
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
-2
@@ -1,7 +1,28 @@
|
|||||||
|
import type { RouteParams } from '@/app/utils';
|
||||||
import { EmbeddableAssistantPage } from '@/components/Embeddable';
|
import { EmbeddableAssistantPage } from '@/components/Embeddable';
|
||||||
|
import { getEmbeddableStaticContext } from '@/lib/embeddable';
|
||||||
|
import { CustomizationAIMode } from '@gitbook/api';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
export const dynamic = 'force-static';
|
export const dynamic = 'force-static';
|
||||||
|
|
||||||
export default async function Page() {
|
type PageProps = {
|
||||||
return <EmbeddableAssistantPage />;
|
params: Promise<RouteParams>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Page(props: PageProps) {
|
||||||
|
const params = await props.params;
|
||||||
|
const { context } = await getEmbeddableStaticContext(params);
|
||||||
|
|
||||||
|
// If the assistant is not enabled, redirect to the docs
|
||||||
|
if (context.customization.ai.mode !== CustomizationAIMode.Assistant) {
|
||||||
|
redirect(`${context.linker.toPathInSite('~gitbook/embed/page/')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EmbeddableAssistantPage
|
||||||
|
baseURL={context.linker.toPathInSite('~gitbook/embed/')}
|
||||||
|
siteTitle={context.site.title}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import type { RouteLayoutParams } from '@/app/utils';
|
||||||
|
import { getEmbeddableStaticContext } from '@/lib/embeddable';
|
||||||
|
import { CustomizationAIMode } from '@gitbook/api';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
export const dynamic = 'force-static';
|
||||||
|
|
||||||
|
type PageProps = {
|
||||||
|
params: Promise<RouteLayoutParams>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Page(props: PageProps) {
|
||||||
|
const params = await props.params;
|
||||||
|
const { context } = await getEmbeddableStaticContext(params);
|
||||||
|
const baseURL = context.linker.toPathInSite('~gitbook/embed/');
|
||||||
|
|
||||||
|
// If assistant is enabled, redirect to assistant, otherwise to docs
|
||||||
|
if (context.customization.ai.mode === CustomizationAIMode.Assistant) {
|
||||||
|
redirect(`${baseURL}/assistant`);
|
||||||
|
} else {
|
||||||
|
redirect(`${baseURL}/page/`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,6 +88,7 @@ export function useAI(): AIContext {
|
|||||||
<AIChatIcon
|
<AIChatIcon
|
||||||
state={chat.loading ? 'thinking' : 'default'}
|
state={chat.loading ? 'thinking' : 'default'}
|
||||||
trademark={config.trademark}
|
trademark={config.trademark}
|
||||||
|
className="size-4"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
open: (query?: string) => {
|
open: (query?: string) => {
|
||||||
|
|||||||
@@ -87,6 +87,19 @@ export type AIChatState = {
|
|||||||
error: boolean;
|
error: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AIChatEvent =
|
||||||
|
| { type: 'open' }
|
||||||
|
| { type: 'postMessage'; message: string }
|
||||||
|
| { type: 'clear' }
|
||||||
|
| { type: 'close' };
|
||||||
|
|
||||||
|
type AIChatEventData<T extends AIChatEvent['type']> = Omit<
|
||||||
|
Extract<AIChatEvent, { type: T }>,
|
||||||
|
'type'
|
||||||
|
>;
|
||||||
|
|
||||||
|
type AIChatEventListener = (input?: Omit<AIChatEvent, 'type'>) => void;
|
||||||
|
|
||||||
export type AIChatController = {
|
export type AIChatController = {
|
||||||
/** Open the dialog */
|
/** Open the dialog */
|
||||||
open: () => void;
|
open: () => void;
|
||||||
@@ -96,6 +109,11 @@ export type AIChatController = {
|
|||||||
postMessage: (input: { message: string }) => void;
|
postMessage: (input: { message: string }) => void;
|
||||||
/** Clear the conversation */
|
/** Clear the conversation */
|
||||||
clear: () => void;
|
clear: () => void;
|
||||||
|
/** Register an event listener */
|
||||||
|
on: <T extends AIChatEvent['type']>(
|
||||||
|
event: T,
|
||||||
|
listener: (input?: AIChatEventData<T>) => void
|
||||||
|
) => () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AIChatControllerContext = React.createContext<AIChatController | null>(null);
|
const AIChatControllerContext = React.createContext<AIChatController | null>(null);
|
||||||
@@ -123,6 +141,17 @@ export function useAIChatState(): AIChatState {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function notify(
|
||||||
|
listeners: AIChatEventListener[] | undefined,
|
||||||
|
input: Omit<AIChatEvent, 'type'>
|
||||||
|
): void {
|
||||||
|
if (!listeners) return;
|
||||||
|
// Defer event listeners to next tick so React can process state updates first
|
||||||
|
setTimeout(() => {
|
||||||
|
listeners.forEach((listener) => listener(input));
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provide the controller to interact with the AI chat.
|
* Provide the controller to interact with the AI chat.
|
||||||
*/
|
*/
|
||||||
@@ -137,6 +166,9 @@ export function AIChatProvider(props: {
|
|||||||
const [, setSearchState] = useSearch();
|
const [, setSearchState] = useSearch();
|
||||||
const language = useLanguage();
|
const language = useLanguage();
|
||||||
|
|
||||||
|
// Event listeners storage
|
||||||
|
const eventsRef = React.useRef<Map<AIChatEvent['type'], AIChatEventListener[]>>(new Map());
|
||||||
|
|
||||||
// Open AI chat and sync with search state
|
// Open AI chat and sync with search state
|
||||||
const onOpen = React.useCallback(() => {
|
const onOpen = React.useCallback(() => {
|
||||||
const { initialQuery } = globalState.getState();
|
const { initialQuery } = globalState.getState();
|
||||||
@@ -149,6 +181,8 @@ export function AIChatProvider(props: {
|
|||||||
scope: prev?.scope ?? 'default',
|
scope: prev?.scope ?? 'default',
|
||||||
open: false, // Close search popover when opening chat
|
open: false, // Close search popover when opening chat
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
notify(eventsRef.current.get('open'), {});
|
||||||
}, [setSearchState]);
|
}, [setSearchState]);
|
||||||
|
|
||||||
// Close AI chat and clear ask parameter
|
// Close AI chat and clear ask parameter
|
||||||
@@ -162,6 +196,8 @@ export function AIChatProvider(props: {
|
|||||||
scope: prev?.scope ?? 'default',
|
scope: prev?.scope ?? 'default',
|
||||||
open: false,
|
open: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
notify(eventsRef.current.get('close'), {});
|
||||||
}, [setSearchState]);
|
}, [setSearchState]);
|
||||||
|
|
||||||
// Stream a message with the AI backend
|
// Stream a message with the AI backend
|
||||||
@@ -379,8 +415,14 @@ export function AIChatProvider(props: {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notify(eventsRef.current.get('postMessage'), { message: input.message });
|
||||||
|
|
||||||
if (query === input.message) {
|
if (query === input.message) {
|
||||||
// Return early if the message is the same as the previous message
|
// Return early if the message is the same as the previous message
|
||||||
|
globalState.setState((state) => ({
|
||||||
|
...state,
|
||||||
|
opened: true,
|
||||||
|
}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,14 +482,34 @@ export function AIChatProvider(props: {
|
|||||||
}));
|
}));
|
||||||
}, [setSearchState]);
|
}, [setSearchState]);
|
||||||
|
|
||||||
|
const onEvent = React.useCallback(
|
||||||
|
<T extends AIChatEvent['type']>(
|
||||||
|
event: T,
|
||||||
|
listener: (input?: AIChatEventData<T>) => void
|
||||||
|
) => {
|
||||||
|
const listeners = eventsRef.current.get(event) || [];
|
||||||
|
listeners.push(listener as AIChatEventListener);
|
||||||
|
eventsRef.current.set(event, listeners);
|
||||||
|
return () => {
|
||||||
|
const currentListeners = eventsRef.current.get(event) || [];
|
||||||
|
eventsRef.current.set(
|
||||||
|
event,
|
||||||
|
currentListeners.filter((l) => l !== listener)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const controller = React.useMemo(() => {
|
const controller = React.useMemo(() => {
|
||||||
return {
|
return {
|
||||||
open: onOpen,
|
open: onOpen,
|
||||||
close: onClose,
|
close: onClose,
|
||||||
clear: onClear,
|
clear: onClear,
|
||||||
postMessage: onPostMessage,
|
postMessage: onPostMessage,
|
||||||
|
on: onEvent,
|
||||||
};
|
};
|
||||||
}, [onOpen, onClose, onClear, onPostMessage]);
|
}, [onOpen, onClose, onClear, onPostMessage, onEvent]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AIChatControllerContext.Provider value={controller}>
|
<AIChatControllerContext.Provider value={controller}>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
EmbeddableFrameButtons,
|
EmbeddableFrameButtons,
|
||||||
EmbeddableFrameHeader,
|
EmbeddableFrameHeader,
|
||||||
EmbeddableFrameHeaderMain,
|
EmbeddableFrameHeaderMain,
|
||||||
|
EmbeddableFrameMain,
|
||||||
EmbeddableFrameSubtitle,
|
EmbeddableFrameSubtitle,
|
||||||
EmbeddableFrameTitle,
|
EmbeddableFrameTitle,
|
||||||
} from '../Embeddable/EmbeddableFrame';
|
} from '../Embeddable/EmbeddableFrame';
|
||||||
@@ -78,29 +79,31 @@ export function AIChat() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<EmbeddableFrame className="relative shrink-0 border-tint-subtle border-l to-tint-base transition-all duration-300 max-lg:circular-corners:rounded-3xl max-lg:rounded-corners:rounded-md max-lg:border lg:w-80 xl:w-96">
|
<EmbeddableFrame className="relative shrink-0 border-tint-subtle border-l to-tint-base transition-all duration-300 max-lg:circular-corners:rounded-3xl max-lg:rounded-corners:rounded-md max-lg:border lg:w-80 xl:w-96">
|
||||||
<EmbeddableFrameHeader>
|
<EmbeddableFrameMain>
|
||||||
<AIChatDynamicIcon trademark={config.trademark} />
|
<EmbeddableFrameHeader>
|
||||||
<EmbeddableFrameHeaderMain>
|
<AIChatDynamicIcon trademark={config.trademark} />
|
||||||
<EmbeddableFrameTitle>
|
<EmbeddableFrameHeaderMain>
|
||||||
{getAIChatName(language, config.trademark)}
|
<EmbeddableFrameTitle>
|
||||||
</EmbeddableFrameTitle>
|
{getAIChatName(language, config.trademark)}
|
||||||
<AIChatSubtitle chat={chat} />
|
</EmbeddableFrameTitle>
|
||||||
</EmbeddableFrameHeaderMain>
|
<AIChatSubtitle chat={chat} />
|
||||||
<EmbeddableFrameButtons>
|
</EmbeddableFrameHeaderMain>
|
||||||
<AIChatControlButton />
|
<EmbeddableFrameButtons>
|
||||||
<Button
|
<AIChatControlButton />
|
||||||
onClick={() => chatController.close()}
|
<Button
|
||||||
iconOnly
|
onClick={() => chatController.close()}
|
||||||
icon="close"
|
iconOnly
|
||||||
label={tString(language, 'close')}
|
icon="close"
|
||||||
variant="blank"
|
label={tString(language, 'close')}
|
||||||
size="default"
|
variant="blank"
|
||||||
/>
|
size="default"
|
||||||
</EmbeddableFrameButtons>
|
/>
|
||||||
</EmbeddableFrameHeader>
|
</EmbeddableFrameButtons>
|
||||||
<EmbeddableFrameBody>
|
</EmbeddableFrameHeader>
|
||||||
<AIChatBody chatController={chatController} chat={chat} />
|
<EmbeddableFrameBody>
|
||||||
</EmbeddableFrameBody>
|
<AIChatBody chatController={chatController} chat={chat} />
|
||||||
|
</EmbeddableFrameBody>
|
||||||
|
</EmbeddableFrameMain>
|
||||||
</EmbeddableFrame>
|
</EmbeddableFrame>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -111,13 +114,14 @@ export function AIChat() {
|
|||||||
*/
|
*/
|
||||||
export function AIChatDynamicIcon(props: {
|
export function AIChatDynamicIcon(props: {
|
||||||
trademark: boolean;
|
trademark: boolean;
|
||||||
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const { trademark } = props;
|
const { trademark, className } = props;
|
||||||
const chat = useAIChatState();
|
const chat = useAIChatState();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AIChatIcon
|
<AIChatIcon
|
||||||
className="size-5 text-tint"
|
className={tcls('size-5 text-tint', className)}
|
||||||
trademark={trademark}
|
trademark={trademark}
|
||||||
state={
|
state={
|
||||||
chat.error
|
chat.error
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useLanguage } from '@/intl/client';
|
import { useLanguage } from '@/intl/client';
|
||||||
import { t, tString } from '@/intl/translate';
|
import { t } from '@/intl/translate';
|
||||||
import { Icon } from '@gitbook/icons';
|
|
||||||
import { useAIChatController, useAIChatState } from '../AI';
|
import { useAIChatController, useAIChatState } from '../AI';
|
||||||
import { Button, DropdownMenu, DropdownMenuItem } from '../primitives';
|
import { Button } from '../primitives';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Button to control the chat (clear, etc.)
|
* Button to control the chat (clear, etc.)
|
||||||
@@ -15,26 +14,16 @@ export function AIChatControlButton() {
|
|||||||
const chatController = useAIChatController();
|
const chatController = useAIChatController();
|
||||||
|
|
||||||
return chat.messages.length > 0 ? (
|
return chat.messages.length > 0 ? (
|
||||||
<DropdownMenu
|
<Button
|
||||||
button={
|
onClick={() => {
|
||||||
<Button
|
chatController.clear();
|
||||||
onClick={() => {}}
|
}}
|
||||||
iconOnly
|
iconOnly
|
||||||
icon="ellipsis"
|
icon="trash-can"
|
||||||
label={tString(language, 'actions')}
|
label={t(language, 'ai_chat_clear_conversation')}
|
||||||
variant="blank"
|
variant="blank"
|
||||||
size="default"
|
size="default"
|
||||||
/>
|
className="animate-blur-in-slow"
|
||||||
}
|
/>
|
||||||
>
|
|
||||||
<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;
|
) : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,20 +18,36 @@ import {
|
|||||||
EmbeddableFrameButtons,
|
EmbeddableFrameButtons,
|
||||||
EmbeddableFrameHeader,
|
EmbeddableFrameHeader,
|
||||||
EmbeddableFrameHeaderMain,
|
EmbeddableFrameHeaderMain,
|
||||||
|
EmbeddableFrameMain,
|
||||||
|
EmbeddableFrameSidebar,
|
||||||
EmbeddableFrameTitle,
|
EmbeddableFrameTitle,
|
||||||
} from './EmbeddableFrame';
|
} from './EmbeddableFrame';
|
||||||
import { EmbeddableIframeButtons, useEmbeddableConfiguration } from './EmbeddableIframeAPI';
|
import {
|
||||||
|
EmbeddableIframeButtons,
|
||||||
|
EmbeddableIframeTabs,
|
||||||
|
useEmbeddableConfiguration,
|
||||||
|
} from './EmbeddableIframeAPI';
|
||||||
|
|
||||||
|
type EmbeddableAIChatProps = {
|
||||||
|
baseURL: string;
|
||||||
|
siteTitle: string;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Embeddable AI chat window in an iframe.
|
* Embeddable AI chat window in an iframe.
|
||||||
*/
|
*/
|
||||||
export function EmbeddableAIChat() {
|
export function EmbeddableAIChat(props: EmbeddableAIChatProps) {
|
||||||
|
const { baseURL, siteTitle } = props;
|
||||||
const chat = useAIChatState();
|
const chat = useAIChatState();
|
||||||
const { config } = useAI();
|
const { config } = useAI();
|
||||||
const chatController = useAIChatController();
|
const chatController = useAIChatController();
|
||||||
const configuration = useEmbeddableConfiguration();
|
const configuration = useEmbeddableConfiguration();
|
||||||
const language = useLanguage();
|
const language = useLanguage();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
chatController.open();
|
||||||
|
}, [chatController]);
|
||||||
|
|
||||||
// Track the view of the AI chat
|
// Track the view of the AI chat
|
||||||
const trackEvent = useTrackEvent();
|
const trackEvent = useTrackEvent();
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -46,28 +62,45 @@ export function EmbeddableAIChat() {
|
|||||||
);
|
);
|
||||||
}, [trackEvent]);
|
}, [trackEvent]);
|
||||||
|
|
||||||
|
const tabsRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EmbeddableFrame>
|
<EmbeddableFrame>
|
||||||
<EmbeddableFrameHeader>
|
<EmbeddableFrameSidebar>
|
||||||
<AIChatDynamicIcon trademark={config.trademark} />
|
<EmbeddableIframeTabs
|
||||||
<EmbeddableFrameHeaderMain>
|
ref={tabsRef}
|
||||||
<EmbeddableFrameTitle>
|
active="assistant"
|
||||||
{getAIChatName(language, config.trademark)}
|
baseURL={baseURL}
|
||||||
</EmbeddableFrameTitle>
|
siteTitle={siteTitle}
|
||||||
<AIChatSubtitle chat={chat} />
|
|
||||||
</EmbeddableFrameHeaderMain>
|
|
||||||
<EmbeddableFrameButtons>
|
|
||||||
<AIChatControlButton />
|
|
||||||
<EmbeddableIframeButtons />
|
|
||||||
</EmbeddableFrameButtons>
|
|
||||||
</EmbeddableFrameHeader>
|
|
||||||
<EmbeddableFrameBody>
|
|
||||||
<AIChatBody
|
|
||||||
chatController={chatController}
|
|
||||||
chat={chat}
|
|
||||||
suggestions={configuration.suggestions}
|
|
||||||
/>
|
/>
|
||||||
</EmbeddableFrameBody>
|
<EmbeddableIframeButtons />
|
||||||
|
</EmbeddableFrameSidebar>
|
||||||
|
<EmbeddableFrameMain>
|
||||||
|
<EmbeddableFrameHeader>
|
||||||
|
{!tabsRef.current ? (
|
||||||
|
<AIChatDynamicIcon
|
||||||
|
className="animate-blur-in-slow"
|
||||||
|
trademark={config.trademark}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<EmbeddableFrameHeaderMain>
|
||||||
|
<EmbeddableFrameTitle>
|
||||||
|
{getAIChatName(language, config.trademark)}
|
||||||
|
</EmbeddableFrameTitle>
|
||||||
|
<AIChatSubtitle chat={chat} />
|
||||||
|
</EmbeddableFrameHeaderMain>
|
||||||
|
<EmbeddableFrameButtons>
|
||||||
|
<AIChatControlButton />
|
||||||
|
</EmbeddableFrameButtons>
|
||||||
|
</EmbeddableFrameHeader>
|
||||||
|
<EmbeddableFrameBody>
|
||||||
|
<AIChatBody
|
||||||
|
chatController={chatController}
|
||||||
|
chat={chat}
|
||||||
|
suggestions={configuration.suggestions}
|
||||||
|
/>
|
||||||
|
</EmbeddableFrameBody>
|
||||||
|
</EmbeddableFrameMain>
|
||||||
</EmbeddableFrame>
|
</EmbeddableFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { EmbeddableAIChat } from './EmbeddableAIChat';
|
import { EmbeddableAIChat } from './EmbeddableAIChat';
|
||||||
|
|
||||||
|
type EmbeddableAssistantPageProps = {
|
||||||
|
baseURL: string;
|
||||||
|
siteTitle: string;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reusable page component for the embed assistant page.
|
* Reusable page component for the embed assistant page.
|
||||||
*/
|
*/
|
||||||
export async function EmbeddableAssistantPage() {
|
export async function EmbeddableAssistantPage(props: EmbeddableAssistantPageProps) {
|
||||||
return <EmbeddableAIChat />;
|
return <EmbeddableAIChat baseURL={props.baseURL} siteTitle={props.siteTitle} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
import { type PagePathParams, getSitePageData } from '@/components/SitePage';
|
import { type PagePathParams, getSitePageData } from '@/components/SitePage';
|
||||||
|
|
||||||
import { PageBody } from '@/components/PageBody';
|
|
||||||
import type { GitBookSiteContext } from '@/lib/context';
|
import type { GitBookSiteContext } from '@/lib/context';
|
||||||
import { SiteInsightsDisplayContext } from '@gitbook/api';
|
import { SiteInsightsDisplayContext } from '@gitbook/api';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { Button } from '../primitives';
|
import { HeaderMobileMenu } from '../Header/HeaderMobileMenu';
|
||||||
|
import { PageBody } from '../PageBody';
|
||||||
|
import { SiteSectionTabs, encodeClientSiteSections } from '../SiteSections';
|
||||||
|
import { TableOfContents } from '../TableOfContents';
|
||||||
|
import { ScrollContainer } from '../primitives/ScrollContainer';
|
||||||
|
import { EmbeddableDocsPageControlButtons } from './EmbeddableDocsPageControlButtons';
|
||||||
import {
|
import {
|
||||||
EmbeddableFrame,
|
EmbeddableFrame,
|
||||||
EmbeddableFrameBody,
|
EmbeddableFrameBody,
|
||||||
EmbeddableFrameButtons,
|
EmbeddableFrameButtons,
|
||||||
EmbeddableFrameHeader,
|
EmbeddableFrameHeader,
|
||||||
EmbeddableFrameHeaderMain,
|
EmbeddableFrameHeaderMain,
|
||||||
|
EmbeddableFrameMain,
|
||||||
|
EmbeddableFrameSidebar,
|
||||||
|
EmbeddableFrameTitle,
|
||||||
} from './EmbeddableFrame';
|
} from './EmbeddableFrame';
|
||||||
import { EmbeddableIframeButtons } from './EmbeddableIframeAPI';
|
import { EmbeddableIframeButtons, EmbeddableIframeTabs } from './EmbeddableIframeAPI';
|
||||||
|
|
||||||
export const dynamic = 'force-static';
|
export const dynamic = 'force-static';
|
||||||
|
|
||||||
@@ -33,32 +39,58 @@ export async function EmbeddableDocsPage(props: EmbeddableDocsPageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<EmbeddableFrame className="site-background">
|
<EmbeddableFrame className="site-background">
|
||||||
<EmbeddableFrameHeader>
|
<EmbeddableFrameSidebar>
|
||||||
<EmbeddableFrameHeaderMain>
|
<EmbeddableIframeTabs
|
||||||
<Button
|
active="docs"
|
||||||
href={context.linker.toPathInSite('~gitbook/embed/assistant')}
|
baseURL={context.linker.toPathInSite('~gitbook/embed/')}
|
||||||
size="default"
|
siteTitle={context.site.title}
|
||||||
variant="blank"
|
/>
|
||||||
icon="arrow-left"
|
<EmbeddableIframeButtons />
|
||||||
label="Back"
|
</EmbeddableFrameSidebar>
|
||||||
/>
|
<EmbeddableFrameMain>
|
||||||
</EmbeddableFrameHeaderMain>
|
<div className="relative flex not-hydrated:animate-blur-in-slow flex-col">
|
||||||
<EmbeddableFrameButtons>
|
<EmbeddableFrameHeader>
|
||||||
<EmbeddableIframeButtons />
|
<HeaderMobileMenu className="-ml-2 page-no-toc:hidden" />
|
||||||
</EmbeddableFrameButtons>
|
<EmbeddableFrameHeaderMain>
|
||||||
</EmbeddableFrameHeader>
|
<EmbeddableFrameTitle>{context.site.title}</EmbeddableFrameTitle>
|
||||||
<EmbeddableFrameBody>
|
</EmbeddableFrameHeaderMain>
|
||||||
<div className="flex-1 overflow-auto p-6">
|
<EmbeddableFrameButtons>
|
||||||
<PageBody
|
<EmbeddableDocsPageControlButtons
|
||||||
context={context}
|
href={context.linker
|
||||||
page={page}
|
.toPathForPage({
|
||||||
ancestors={ancestors}
|
pages: context.revision.pages,
|
||||||
document={document}
|
page,
|
||||||
withPageFeedback={withPageFeedback}
|
})
|
||||||
insightsDisplayContext={SiteInsightsDisplayContext.Embed}
|
.replace(/~gitbook\/embed\/page\/?/, '')}
|
||||||
/>
|
/>
|
||||||
|
</EmbeddableFrameButtons>
|
||||||
|
</EmbeddableFrameHeader>
|
||||||
|
{context.sections ? (
|
||||||
|
<SiteSectionTabs
|
||||||
|
className="-mt-2 border-tint-subtle border-b"
|
||||||
|
sections={encodeClientSiteSections(context, context.sections)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</EmbeddableFrameBody>
|
<EmbeddableFrameBody>
|
||||||
|
<ScrollContainer
|
||||||
|
orientation="vertical"
|
||||||
|
className="not-hydrated:animate-blur-in-slow"
|
||||||
|
contentClassName="p-4"
|
||||||
|
fadeEdges={context.sections ? [] : ['leading']}
|
||||||
|
>
|
||||||
|
<TableOfContents className="pt-0" context={context} />
|
||||||
|
<PageBody
|
||||||
|
context={context}
|
||||||
|
page={page}
|
||||||
|
ancestors={ancestors}
|
||||||
|
document={document}
|
||||||
|
withPageFeedback={withPageFeedback}
|
||||||
|
insightsDisplayContext={SiteInsightsDisplayContext.Embed}
|
||||||
|
/>
|
||||||
|
</ScrollContainer>
|
||||||
|
</EmbeddableFrameBody>
|
||||||
|
</EmbeddableFrameMain>
|
||||||
</EmbeddableFrame>
|
</EmbeddableFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { tString, useLanguage } from '@/intl/client';
|
||||||
|
import { Button } from '../primitives';
|
||||||
|
|
||||||
|
export function EmbeddableDocsPageControlButtons(props: { href: string }) {
|
||||||
|
const { href } = props;
|
||||||
|
const language = useLanguage();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
icon="arrow-up-right-from-square"
|
||||||
|
label={tString(language, 'open_in_new_tab')}
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
iconOnly
|
||||||
|
variant="blank"
|
||||||
|
size="default"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ export const EmbeddableFrame = React.forwardRef<HTMLDivElement, EmbeddableFrameP
|
|||||||
<div
|
<div
|
||||||
{...divProps}
|
{...divProps}
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'flex h-full grow flex-col overflow-hidden bg-radial-[circle_at_bottom] from-primary to-50% to-transparent text-sm text-tint',
|
'flex h-full grow overflow-hidden bg-radial-[circle_at_bottom] from-primary to-50% to-transparent text-sm text-tint',
|
||||||
divProps.className
|
divProps.className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -28,13 +28,19 @@ export const EmbeddableFrame = React.forwardRef<HTMLDivElement, EmbeddableFrameP
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export function EmbeddableFrameMain(props: { children: React.ReactNode }) {
|
||||||
|
const { children } = props;
|
||||||
|
|
||||||
|
return <div className="flex flex-1 flex-col overflow-hidden">{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
export function EmbeddableFrameHeader(props: {
|
export function EmbeddableFrameHeader(props: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex animate-fade-in-slow select-none items-center gap-2 px-4 py-2 text-tint-strong">
|
<div className="relative z-10 flex not-hydrated:animate-blur-in-slow select-none items-center gap-2 px-4 py-2.5 text-tint-strong">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -45,7 +51,7 @@ export function EmbeddableFrameHeaderMain(props: {
|
|||||||
}) {
|
}) {
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
|
|
||||||
return <div className="flex flex-1 flex-col">{children}</div>;
|
return <div className="flex h-8 flex-1 flex-col justify-center">{children}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EmbeddableFrameBody(props: {
|
export function EmbeddableFrameBody(props: {
|
||||||
@@ -82,10 +88,21 @@ export function EmbeddableFrameSubtitle(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EmbeddableFrameButtons(props: {
|
export function EmbeddableFrameSidebar(props: { children: React.ReactNode }) {
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
|
|
||||||
return <div className="-mr-2 ml-auto flex gap-2">{children}</div>;
|
return (
|
||||||
|
<div className="flex w-13 shrink-0 origin-top not-hydrated:animate-blur-in-slow flex-col gap-2 overflow-hidden border-tint-solid/3 border-r bg-tint-solid/1 p-2 transition-all transition-discrete duration-300 empty:hidden empty:w-0 empty:px-0">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmbeddableFrameButtons(props: {
|
||||||
|
className?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const { children, className } = props;
|
||||||
|
|
||||||
|
return <div className={tcls('flex gap-2', className)}>{children}</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,17 @@ import type { GitBookEmbeddableConfiguration, ParentToFrameMessage } from '@gitb
|
|||||||
import { createChannel } from 'bidc';
|
import { createChannel } from 'bidc';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { useAIChatController } from '@/components/AI';
|
import { useAI, useAIChatController } from '@/components/AI';
|
||||||
|
import { CustomizationAIMode } from '@gitbook/api';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { createStore, useStore } from 'zustand';
|
import { createStore, useStore } from 'zustand';
|
||||||
import { integrationsAssistantTools } from '../Integrations';
|
import { integrationsAssistantTools } from '../Integrations';
|
||||||
import { Button } from '../primitives';
|
import { Button } from '../primitives';
|
||||||
|
|
||||||
const embeddableConfiguration = createStore<GitBookEmbeddableConfiguration>(() => ({
|
const embeddableConfiguration = createStore<GitBookEmbeddableConfiguration>(() => ({
|
||||||
buttons: [],
|
tabs: [],
|
||||||
welcomeMessage: '',
|
actions: [],
|
||||||
|
greeting: { title: '', subtitle: '' },
|
||||||
suggestions: [],
|
suggestions: [],
|
||||||
tools: [],
|
tools: [],
|
||||||
}));
|
}));
|
||||||
@@ -28,6 +30,12 @@ export function EmbeddableIframeAPI(props: {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const chatController = useAIChatController();
|
const chatController = useAIChatController();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
return chatController.on('open', () => {
|
||||||
|
router.push(`${baseURL}/assistant`);
|
||||||
|
});
|
||||||
|
}, [router, baseURL, chatController]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (window.parent === window) {
|
if (window.parent === window) {
|
||||||
return;
|
return;
|
||||||
@@ -93,23 +101,114 @@ export function useEmbeddableConfiguration<T = GitBookEmbeddableConfiguration>(
|
|||||||
* Display the buttons defined by the parent window.
|
* Display the buttons defined by the parent window.
|
||||||
*/
|
*/
|
||||||
export function EmbeddableIframeButtons() {
|
export function EmbeddableIframeButtons() {
|
||||||
const buttons = useEmbeddableConfiguration((state) => state.buttons);
|
const { actions: configuredActions, buttons: configuredButtons = [] } =
|
||||||
|
useEmbeddableConfiguration((state) => state);
|
||||||
|
const actions = configuredActions.length > 0 ? configuredActions : configuredButtons;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{buttons.map((button) => (
|
{actions.length > 0 && (
|
||||||
|
<hr className="my-2 border-0 border-tint-subtle border-b first:hidden" />
|
||||||
|
)}
|
||||||
|
{actions.map((action, index) => (
|
||||||
<Button
|
<Button
|
||||||
key={button.label}
|
key={action.label}
|
||||||
size="default"
|
size="default"
|
||||||
variant="blank"
|
variant="blank"
|
||||||
icon={button.icon}
|
icon={action?.icon ?? 'square-question'}
|
||||||
label={button.label}
|
label={action?.label}
|
||||||
iconOnly
|
iconOnly
|
||||||
|
className="not-hydrated:animate-blur-in-slow [&_.button-leading-icon]:size-5"
|
||||||
|
disabled={!action.onClick}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
button.onClick();
|
action.onClick?.();
|
||||||
}}
|
}}
|
||||||
|
tooltipProps={{
|
||||||
|
contentProps: {
|
||||||
|
side: 'right',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
style={{ animationDelay: `${index * 100}ms` }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function EmbeddableIframeTabs(props: {
|
||||||
|
ref?: React.RefObject<HTMLDivElement | null>;
|
||||||
|
active?: string;
|
||||||
|
baseURL: string;
|
||||||
|
siteTitle: string;
|
||||||
|
}) {
|
||||||
|
const { ref, active = 'assistant', baseURL, siteTitle } = props;
|
||||||
|
const { tabs: configuredTabs, actions } = useEmbeddableConfiguration();
|
||||||
|
|
||||||
|
const { assistants, config } = useAI();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
config.aiMode === CustomizationAIMode.Assistant &&
|
||||||
|
assistants[0] &&
|
||||||
|
(configuredTabs.includes('assistant') || configuredTabs.length === 0)
|
||||||
|
? {
|
||||||
|
key: 'assistant',
|
||||||
|
label: assistants[0].label,
|
||||||
|
icon: assistants[0].icon,
|
||||||
|
onClick: () => {
|
||||||
|
router.push(`${baseURL}/assistant`);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
configuredTabs.includes('docs') || configuredTabs.length === 0
|
||||||
|
? {
|
||||||
|
key: 'docs',
|
||||||
|
label: siteTitle,
|
||||||
|
icon: 'book-open',
|
||||||
|
onClick: () => {
|
||||||
|
router.push(`${baseURL}/page/`);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
].filter((tab) => tab !== null);
|
||||||
|
|
||||||
|
// Override the active tab if it doesn't match the configured tabs
|
||||||
|
React.useEffect(() => {
|
||||||
|
const hasAssistant = tabs.find((tab) => tab.key === 'assistant');
|
||||||
|
const hasDocs = tabs.find((tab) => tab.key === 'docs');
|
||||||
|
if (!hasAssistant && !hasDocs) {
|
||||||
|
// No valid tabs, do not redirect
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (active === 'assistant' && !hasAssistant) {
|
||||||
|
router.replace(`${baseURL}/page`);
|
||||||
|
} else if (active === 'docs' && !hasDocs) {
|
||||||
|
router.replace(`${baseURL}/assistant`);
|
||||||
|
}
|
||||||
|
}, [tabs, baseURL, router, active]);
|
||||||
|
|
||||||
|
return tabs.length > 1 || actions.length > 0 ? (
|
||||||
|
<div className="flex flex-col gap-2" ref={ref}>
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<Button
|
||||||
|
key={tab.key}
|
||||||
|
data-testid={`embed-tab-${tab.key}`}
|
||||||
|
label={tab.label}
|
||||||
|
size="default"
|
||||||
|
variant="blank"
|
||||||
|
icon={tab.icon}
|
||||||
|
active={tab.key === active}
|
||||||
|
className="not-hydrated:animate-blur-in-slow [&_.button-leading-icon]:size-5"
|
||||||
|
iconOnly
|
||||||
|
onClick={tab.onClick}
|
||||||
|
tooltipProps={{
|
||||||
|
contentProps: {
|
||||||
|
side: 'right',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import {
|
|||||||
} from '@/components/SiteLayout';
|
} from '@/components/SiteLayout';
|
||||||
import type { VisitorAuthClaims } from '@/lib/adaptive';
|
import type { VisitorAuthClaims } from '@/lib/adaptive';
|
||||||
import type { GitBookSiteContext } from '@/lib/context';
|
import type { GitBookSiteContext } from '@/lib/context';
|
||||||
import { CustomizationAIMode } from '@gitbook/api';
|
import { SiteInsightsTrademarkPlacement } from '@gitbook/api';
|
||||||
import { SpaceLayoutServerContext } from '../SpaceLayout';
|
import { SpaceLayoutServerContext } from '../SpaceLayout';
|
||||||
|
import { TrademarkLink } from '../TableOfContents/Trademark';
|
||||||
|
import { NavigationLoader } from '../primitives/NavigationLoader';
|
||||||
import { EmbeddableIframeAPI } from './EmbeddableIframeAPI';
|
import { EmbeddableIframeAPI } from './EmbeddableIframeAPI';
|
||||||
|
|
||||||
type EmbeddableRootLayoutProps = {
|
type EmbeddableRootLayoutProps = {
|
||||||
@@ -29,12 +31,16 @@ export async function EmbeddableRootLayout({
|
|||||||
return (
|
return (
|
||||||
<CustomizationRootLayout context={context}>
|
<CustomizationRootLayout context={context}>
|
||||||
<SiteLayoutClientContexts
|
<SiteLayoutClientContexts
|
||||||
forcedTheme={context.customization.themes.default}
|
forcedTheme={
|
||||||
|
context.customization.themes.toggeable
|
||||||
|
? undefined
|
||||||
|
: context.customization.themes.default
|
||||||
|
}
|
||||||
externalLinksTarget={context.customization.externalLinks.target}
|
externalLinksTarget={context.customization.externalLinks.target}
|
||||||
contextId={context.contextId}
|
contextId={context.contextId}
|
||||||
>
|
>
|
||||||
<AIContextProvider
|
<AIContextProvider
|
||||||
aiMode={CustomizationAIMode.Assistant}
|
aiMode={context.customization.ai.mode}
|
||||||
trademark={context.customization.trademark.enabled}
|
trademark={context.customization.trademark.enabled}
|
||||||
>
|
>
|
||||||
<SpaceLayoutServerContext
|
<SpaceLayoutServerContext
|
||||||
@@ -46,9 +52,19 @@ export async function EmbeddableRootLayout({
|
|||||||
asEmbeddable: true,
|
asEmbeddable: true,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 flex flex-col">{children}</div>
|
<NavigationLoader />
|
||||||
|
<div className="fixed inset-0 flex flex-col">
|
||||||
|
{children}
|
||||||
|
{context.customization.trademark.enabled ? (
|
||||||
|
<TrademarkLink
|
||||||
|
className="rounded-none border-tint-solid/3 border-t bg-tint-solid/1 px-4 py-2.5 text-tint/8 ring-0"
|
||||||
|
context={context}
|
||||||
|
placement={SiteInsightsTrademarkPlacement.Embed}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
<EmbeddableIframeAPI
|
<EmbeddableIframeAPI
|
||||||
baseURL={context.linker.toPathInSpace('~gitbook/embed/')}
|
baseURL={context.linker.toPathInSite('~gitbook/embed/')}
|
||||||
/>
|
/>
|
||||||
</SpaceLayoutServerContext>
|
</SpaceLayoutServerContext>
|
||||||
</AIContextProvider>
|
</AIContextProvider>
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Icon } from '@gitbook/icons';
|
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { tString, useLanguage } from '@/intl/client';
|
import { tString, useLanguage } from '@/intl/client';
|
||||||
import { tcls } from '@/lib/tailwind';
|
|
||||||
|
|
||||||
import { useScrollListener } from '../hooks/useScrollListener';
|
import { useScrollListener } from '../hooks/useScrollListener';
|
||||||
|
import { Button, type ButtonProps } from '../primitives';
|
||||||
|
|
||||||
const globalClassName = 'navigation-open';
|
const globalClassName = 'navigation-open';
|
||||||
|
|
||||||
@@ -16,18 +14,22 @@ const SCROLL_DISTANCE = 320;
|
|||||||
/**
|
/**
|
||||||
* Button to show/hide the table of content on mobile.
|
* Button to show/hide the table of content on mobile.
|
||||||
*/
|
*/
|
||||||
export function HeaderMobileMenu(props: Partial<React.ButtonHTMLAttributes<HTMLButtonElement>>) {
|
export function HeaderMobileMenu(props: ButtonProps) {
|
||||||
const language = useLanguage();
|
const language = useLanguage();
|
||||||
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const hasScrollRef = useRef(false);
|
const hasScrollRef = useRef(false);
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const toggleNavigation = () => {
|
const toggleNavigation = () => {
|
||||||
if (!hasScrollRef.current && document.body.classList.contains(globalClassName)) {
|
if (!hasScrollRef.current && document.body.classList.contains(globalClassName)) {
|
||||||
document.body.classList.remove(globalClassName);
|
document.body.classList.remove(globalClassName);
|
||||||
|
setIsOpen(false);
|
||||||
} else {
|
} else {
|
||||||
document.body.classList.add(globalClassName);
|
document.body.classList.add(globalClassName);
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
|
setIsOpen(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -42,16 +44,15 @@ export function HeaderMobileMenu(props: Partial<React.ButtonHTMLAttributes<HTMLB
|
|||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
{...props}
|
icon="bars"
|
||||||
aria-label={tString(language, 'table_of_contents_button_label')}
|
iconOnly
|
||||||
|
variant="blank"
|
||||||
|
size="default"
|
||||||
|
label={tString(language, 'table_of_contents_button_label')}
|
||||||
onClick={toggleNavigation}
|
onClick={toggleNavigation}
|
||||||
className={tcls(
|
active={isOpen}
|
||||||
'flex flex-row items-center rounded-sm straight-corners:rounded-xs px-2 py-1',
|
{...props}
|
||||||
props.className
|
/>
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon icon="bars" className="size-4 text-inherit" />
|
|
||||||
</button>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export function SiteSectionTabs(props: {
|
|||||||
children,
|
children,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||||
const currentTriggerRef = React.useRef<HTMLButtonElement | null>(null);
|
const currentTriggerRef = React.useRef<HTMLButtonElement | null>(null);
|
||||||
const [offset, setOffset] = React.useState<number | null>(null);
|
const [offset, setOffset] = React.useState<number | null>(null);
|
||||||
const [value, setValue] = React.useState<string | undefined>();
|
const [value, setValue] = React.useState<string | undefined>();
|
||||||
@@ -41,12 +42,15 @@ export function SiteSectionTabs(props: {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const trigger = currentTriggerRef.current;
|
const trigger = currentTriggerRef.current;
|
||||||
if (!value || !trigger) {
|
const container = containerRef.current;
|
||||||
|
if (!value || !trigger || !container) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const triggerWidth = trigger.getBoundingClientRect().width;
|
const triggerWidth = trigger.getBoundingClientRect().width - SCREEN_OFFSET;
|
||||||
const triggerLeft = trigger.getBoundingClientRect().left;
|
const triggerLeft =
|
||||||
|
trigger.getBoundingClientRect().left -
|
||||||
|
(window.innerWidth - container.getBoundingClientRect().width) / 2;
|
||||||
setOffset(triggerLeft + triggerWidth / 2);
|
setOffset(triggerLeft + triggerWidth / 2);
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
@@ -58,6 +62,7 @@ export function SiteSectionTabs(props: {
|
|||||||
'page-default-width:2xl:px-[calc((100%-1536px+4rem)/2)]',
|
'page-default-width:2xl:px-[calc((100%-1536px+4rem)/2)]',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
ref={containerRef}
|
||||||
value={value}
|
value={value}
|
||||||
onValueChange={setValue}
|
onValueChange={setValue}
|
||||||
skipDelayDuration={500}
|
skipDelayDuration={500}
|
||||||
@@ -141,14 +146,14 @@ export function SiteSectionTabs(props: {
|
|||||||
{children}
|
{children}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="fixed top-full left-0 z-20 flex w-full"
|
className="absolute top-full left-0 z-20 flex w-full"
|
||||||
style={{
|
style={{
|
||||||
padding: `0 ${SCREEN_OFFSET}px 0 ${SCREEN_OFFSET}px`,
|
padding: `0 ${SCREEN_OFFSET}px 0 ${SCREEN_OFFSET}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<NavigationMenu.Viewport
|
<NavigationMenu.Viewport
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'relative origin-top overflow-auto circular-corners:rounded-3xl rounded-corners:rounded-xl border border-tint bg-tint-base shadow-lg transition-transform duration-250 ease-in-out',
|
'relative origin-top overflow-auto circular-corners:rounded-3xl rounded-corners:rounded-xl border border-tint bg-tint-base shadow-lg ease-in-out',
|
||||||
'-mt-0.5 w-full md:w-max',
|
'-mt-0.5 w-full md:w-max',
|
||||||
'max-h-[calc(100vh-8rem)] data-[state=closed]:animate-scale-out data-[state=open]:animate-scale-in',
|
'max-h-[calc(100vh-8rem)] data-[state=closed]:animate-scale-out data-[state=open]:animate-scale-in',
|
||||||
"[&:not([style*='--radix-navigation-menu-viewport-width'])]:hidden" // The viewport width is only calculated once it's triggered, and can take a while. We hide the viewport until it's ready.
|
"[&:not([style*='--radix-navigation-menu-viewport-width'])]:hidden" // The viewport width is only calculated once it's triggered, and can take a while. We hide the viewport until it's ready.
|
||||||
@@ -156,7 +161,7 @@ export function SiteSectionTabs(props: {
|
|||||||
style={{
|
style={{
|
||||||
translate:
|
translate:
|
||||||
!isMobile && offset
|
!isMobile && offset
|
||||||
? `clamp(0px, calc(${offset}px - ${SCREEN_OFFSET}px - 50%), calc(100vw - var(--radix-navigation-menu-viewport-width, 0px) - ${SCREEN_OFFSET * 3}px)) 0 0`
|
? `clamp(0px, calc(${offset}px - var(--radix-navigation-menu-viewport-width, 0px)/2), calc(100vw - var(--radix-navigation-menu-viewport-width, 0px) - ${SCREEN_OFFSET * 3}px)) 0 0`
|
||||||
: '0 0 0', // TranslateZ is needed to force a stacking context, fixing a rendering bug on Safari
|
: '0 0 0', // TranslateZ is needed to force a stacking context, fixing a rendering bug on Safari
|
||||||
display: offset === null ? 'none' : undefined,
|
display: offset === null ? 'none' : undefined,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ export async function TableOfContents(props: {
|
|||||||
context: GitBookSiteContext;
|
context: GitBookSiteContext;
|
||||||
header?: React.ReactNode; // Displayed outside the scrollable TOC as a sticky header
|
header?: React.ReactNode; // Displayed outside the scrollable TOC as a sticky header
|
||||||
innerHeader?: React.ReactNode; // Displayed outside the scrollable TOC, directly above the page list
|
innerHeader?: React.ReactNode; // Displayed outside the scrollable TOC, directly above the page list
|
||||||
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const { innerHeader, context, header } = props;
|
const { innerHeader, context, header, className } = props;
|
||||||
const { customization, revision } = context;
|
const { customization, revision } = context;
|
||||||
|
|
||||||
const pages = await encodeClientTableOfContents(context, revision.pages, revision.pages);
|
const pages = await encodeClientTableOfContents(context, revision.pages, revision.pages);
|
||||||
@@ -74,7 +75,8 @@ export async function TableOfContents(props: {
|
|||||||
'gap-4',
|
'gap-4',
|
||||||
|
|
||||||
'navigation-open:border-b',
|
'navigation-open:border-b',
|
||||||
'border-tint-subtle'
|
'border-tint-subtle',
|
||||||
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{header && header}
|
{header && header}
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import { Link } from '../primitives';
|
|||||||
export function Trademark(props: {
|
export function Trademark(props: {
|
||||||
context: GitBookSpaceContext;
|
context: GitBookSpaceContext;
|
||||||
placement: SiteInsightsTrademarkPlacement;
|
placement: SiteInsightsTrademarkPlacement;
|
||||||
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const { className, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={tcls(
|
className={tcls(
|
||||||
@@ -55,10 +57,12 @@ export function Trademark(props: {
|
|||||||
'[html.sidebar-filled.theme-bold.tint_&]:before:to-tint-subtle',
|
'[html.sidebar-filled.theme-bold.tint_&]:before:to-tint-subtle',
|
||||||
'[html.sidebar-filled.theme-muted_&]:before:to-tint-base',
|
'[html.sidebar-filled.theme-muted_&]:before:to-tint-base',
|
||||||
'[html.sidebar-filled.theme-bold.tint_&]:before:to-tint-base',
|
'[html.sidebar-filled.theme-bold.tint_&]:before:to-tint-base',
|
||||||
'page-no-toc:before:to-transparent!'
|
'page-no-toc:before:to-transparent!',
|
||||||
|
|
||||||
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TrademarkLink {...props} />
|
<TrademarkLink {...rest} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -69,8 +73,9 @@ export function Trademark(props: {
|
|||||||
export function TrademarkLink(props: {
|
export function TrademarkLink(props: {
|
||||||
context: GitBookSpaceContext;
|
context: GitBookSpaceContext;
|
||||||
placement: SiteInsightsTrademarkPlacement;
|
placement: SiteInsightsTrademarkPlacement;
|
||||||
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const { context, placement } = props;
|
const { context, placement, className } = props;
|
||||||
const { space } = context;
|
const { space } = context;
|
||||||
const language = getSpaceLanguage(context);
|
const language = getSpaceLanguage(context);
|
||||||
|
|
||||||
@@ -85,9 +90,6 @@ export function TrademarkLink(props: {
|
|||||||
href={url.toString()}
|
href={url.toString()}
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'text-sm',
|
'text-sm',
|
||||||
// 'lg:max-xl:page-no-toc:text-xs',
|
|
||||||
// 'lg:max-xl:page-no-toc:px-3',
|
|
||||||
// 'lg:max-xl:page-no-toc:py-3',
|
|
||||||
'font-semibold',
|
'font-semibold',
|
||||||
'text-tint',
|
'text-tint',
|
||||||
|
|
||||||
@@ -113,29 +115,17 @@ export function TrademarkLink(props: {
|
|||||||
'ring-tint-subtle',
|
'ring-tint-subtle',
|
||||||
|
|
||||||
'transition-colors',
|
'transition-colors',
|
||||||
'pointer-events-auto'
|
'pointer-events-auto',
|
||||||
|
|
||||||
|
className
|
||||||
)}
|
)}
|
||||||
insights={{
|
insights={{
|
||||||
type: 'trademark_click',
|
type: 'trademark_click',
|
||||||
placement,
|
placement,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon icon="gitbook" className={tcls('size-5', 'shrink-0')} />
|
||||||
icon="gitbook"
|
<span className={tcls('ml-3')}>{t(language, 'powered_by_gitbook')}</span>
|
||||||
className={tcls(
|
|
||||||
'size-5',
|
|
||||||
// 'lg:max-xl:page-no-toc:size-4',
|
|
||||||
'shrink-0'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className={tcls(
|
|
||||||
'ml-3'
|
|
||||||
// 'lg:max-xl:page-no-toc:ml-2'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{t(language, 'powered_by_gitbook')}
|
|
||||||
</span>
|
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { type ClassValue, tcls } from '@/lib/tailwind';
|
|||||||
import { Icon, type IconName } from '@gitbook/icons';
|
import { Icon, type IconName } from '@gitbook/icons';
|
||||||
import { Link, type LinkInsightsProps } from './Link';
|
import { Link, type LinkInsightsProps } from './Link';
|
||||||
import { useClassnames } from './StyleProvider';
|
import { useClassnames } from './StyleProvider';
|
||||||
import { Tooltip } from './Tooltip';
|
import { Tooltip, type TooltipProps } from './Tooltip';
|
||||||
|
|
||||||
export type ButtonProps = {
|
export type ButtonProps = {
|
||||||
href?: string;
|
href?: string;
|
||||||
@@ -20,6 +20,7 @@ export type ButtonProps = {
|
|||||||
trailing?: React.ReactNode;
|
trailing?: React.ReactNode;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
|
tooltipProps?: TooltipProps;
|
||||||
} & LinkInsightsProps &
|
} & LinkInsightsProps &
|
||||||
React.HTMLAttributes<HTMLElement>;
|
React.HTMLAttributes<HTMLElement>;
|
||||||
|
|
||||||
@@ -112,6 +113,7 @@ export const Button = React.forwardRef<
|
|||||||
active,
|
active,
|
||||||
trailing,
|
trailing,
|
||||||
disabled,
|
disabled,
|
||||||
|
tooltipProps,
|
||||||
...rest
|
...rest
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
@@ -133,18 +135,29 @@ export const Button = React.forwardRef<
|
|||||||
);
|
);
|
||||||
const buttonOnlyClassNames = useClassnames(['ButtonStyles']);
|
const buttonOnlyClassNames = useClassnames(['ButtonStyles']);
|
||||||
|
|
||||||
|
let iconElement = null;
|
||||||
|
if (icon) {
|
||||||
|
if (React.isValidElement(icon)) {
|
||||||
|
type IconElement = React.ReactElement<React.SVGProps<SVGSVGElement>>;
|
||||||
|
iconElement = React.cloneElement(icon as IconElement, {
|
||||||
|
className: tcls(
|
||||||
|
'button-leading-icon size-[1em] shrink-0',
|
||||||
|
(icon as IconElement).props.className
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
iconElement = (
|
||||||
|
<Icon
|
||||||
|
icon={icon as IconName}
|
||||||
|
className={tcls('button-leading-icon size-[1em] shrink-0')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<>
|
<>
|
||||||
{icon ? (
|
{iconElement}
|
||||||
typeof icon === 'string' ? (
|
|
||||||
<Icon
|
|
||||||
icon={icon as IconName}
|
|
||||||
className={tcls('button-leading-icon size-[1em] shrink-0')}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
icon
|
|
||||||
)
|
|
||||||
) : null}
|
|
||||||
{iconOnly || (!children && !label) ? null : (
|
{iconOnly || (!children && !label) ? null : (
|
||||||
<span className="button-content truncate">{children ?? label}</span>
|
<span className="button-content truncate">{children ?? label}</span>
|
||||||
)}
|
)}
|
||||||
@@ -184,9 +197,13 @@ export const Button = React.forwardRef<
|
|||||||
|
|
||||||
return (children || iconOnly) && label ? (
|
return (children || iconOnly) && label ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
rootProps={{ open: disabled === true ? false : undefined }}
|
rootProps={{
|
||||||
|
open: disabled === true ? false : undefined,
|
||||||
|
...tooltipProps?.rootProps,
|
||||||
|
}}
|
||||||
label={label}
|
label={label}
|
||||||
triggerProps={{ disabled }}
|
triggerProps={{ disabled, ...tooltipProps?.triggerProps }}
|
||||||
|
contentProps={{ ...tooltipProps?.contentProps }}
|
||||||
>
|
>
|
||||||
{button}
|
{button}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ export function Link(props: LinkProps) {
|
|||||||
|
|
||||||
const onClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
const onClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||||
const isExternalWithOrigin = isExternalLink(href, window.location.origin);
|
const isExternalWithOrigin = isExternalLink(href, window.location.origin);
|
||||||
// Only trigger navigation context for internal links without modifier keys (i.e. open in new tab).
|
// Only trigger navigation context for internal links in the same window without modifier keys (i.e. open in new tab).
|
||||||
if (!isExternal && !event.ctrlKey && !event.metaKey) {
|
if (!isExternal && target !== '_blank' && !event.ctrlKey && !event.metaKey) {
|
||||||
onNavigationClick(href);
|
onNavigationClick(href);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ import { tcls } from '@/lib/tailwind';
|
|||||||
import * as RadixTooltip from '@radix-ui/react-tooltip';
|
import * as RadixTooltip from '@radix-ui/react-tooltip';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export type TooltipProps = {
|
||||||
|
rootProps?: RadixTooltip.TooltipProps;
|
||||||
|
triggerProps?: RadixTooltip.TooltipTriggerProps;
|
||||||
|
contentProps?: RadixTooltip.TooltipContentProps;
|
||||||
|
};
|
||||||
|
|
||||||
export function Tooltip(props: {
|
export function Tooltip(props: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
label?: string | React.ReactNode;
|
label?: string | React.ReactNode;
|
||||||
|
|||||||
@@ -55,5 +55,16 @@ export function getEmbeddableLinker(linker: GitBookLinker): GitBookLinker {
|
|||||||
spaceBasePath: joinPath(override.spaceBasePath, '~gitbook/embed/page'),
|
spaceBasePath: joinPath(override.spaceBasePath, '~gitbook/embed/page'),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toLinkForContent(rawURL: string): string {
|
||||||
|
const result = linker.toLinkForContent(rawURL);
|
||||||
|
// If the link is not relative or already an embed, return it as is
|
||||||
|
if (result.includes('~gitbook/embed') || !result.startsWith('/')) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the link is relative, assume it's a section link and append the embed path
|
||||||
|
return joinPath(result, '~gitbook/embed/page');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -612,6 +612,7 @@ function encodePathInSiteContent(rawPathname: string): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (pathname) {
|
switch (pathname) {
|
||||||
|
case '~gitbook/embed':
|
||||||
case '~gitbook/embed/assistant':
|
case '~gitbook/embed/assistant':
|
||||||
case '~gitbook/icon':
|
case '~gitbook/icon':
|
||||||
return { pathname };
|
return { pathname };
|
||||||
|
|||||||
Reference in New Issue
Block a user