Skip to content

Commit ea61a93

Browse files
committed
Review
1 parent 8ee4794 commit ea61a93

File tree

4 files changed

+63
-29
lines changed

4 files changed

+63
-29
lines changed

packages/embed/README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ The standalone script provides a global `GitBook` function. See the [API Referen
3838
GitBook('configure', {
3939
button: {
4040
label: 'Ask',
41-
icon: 'assistant' // 'assistant' | 'sparkle' | 'circle-question' | 'book'
41+
icon: 'assistant' // 'assistant' | 'sparkle' | 'help' | 'book'
4242
},
4343
tabs: ['assistant', 'docs'],
4444
actions: [
@@ -238,16 +238,18 @@ Available in: Standalone script, NPM package, React components
238238

239239
Custom action buttons rendered in the sidebar alongside tabs. Each action button triggers a callback when clicked.
240240

241+
**Note**: This prop was previously named `buttons`. Use `actions` instead, it has the same functionality.
242+
241243
- **Type**: `GitBookEmbeddableActionDefinition[]`
242244
- **Properties**:
243-
- `icon`: `string` - Icon name (e.g., `'circle-question'`, `'book'`, `'sparkle'`, `'rocket'`, `'assistant'`)
245+
- `icon`: `string` - Icon name. Any [FontAwesome icon](https://fontawesome.com/search) is supported. (e.g., `'rocket'`, `'comments'`, `'user-circle'`, ...)
244246
- `label`: `string` - Button label text
245247
- `onClick`: `() => void | Promise<void>` - Callback function when clicked
246248

247249
```javascript
248250
actions: [
249251
{
250-
icon: 'circle-question',
252+
icon: 'comments',
251253
label: 'Contact Support',
252254
onClick: () => window.open('https://support.example.com', '_blank')
253255
},
@@ -261,8 +263,6 @@ actions: [
261263
]
262264
```
263265

264-
**Note**: This prop was previously named `buttons`. Use `actions` instead, it has the same functionality.
265-
266266
### `greeting`
267267

268268
Available in: Standalone script, NPM package, React components
@@ -398,10 +398,10 @@ Available in: Standalone script only
398398

399399
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.
400400

401-
- **Type**: `{ label: string, icon: 'assistant' | 'sparkle' | 'circle-question' | 'book' }`
401+
- **Type**: `{ label: string, icon: 'assistant' | 'sparkle' | 'help' | 'book' }`
402402
- **Properties**:
403403
- `label`: `string` - Button label text
404-
- `icon`: `'assistant' | 'sparkle' | 'circle-question' | 'book'` - Icon displayed on the button
404+
- `icon`: `'assistant' | 'sparkle' | 'help' | 'book'` - Icon displayed on the button. Choose from one of 4 presets.
405405

406406
```javascript
407407
button: {

packages/gitbook/src/components/AI/useAIChat.tsx

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,19 @@ export type AIChatState = {
8787
error: boolean;
8888
};
8989

90+
export type AIChatEvent =
91+
| { type: 'open' }
92+
| { type: 'postMessage'; message: string }
93+
| { type: 'clear' }
94+
| { type: 'close' };
95+
96+
type AIChatEventData<T extends AIChatEvent['type']> = Omit<
97+
Extract<AIChatEvent, { type: T }>,
98+
'type'
99+
>;
100+
101+
type AIChatEventListener = (input?: Omit<AIChatEvent, 'type'>) => void;
102+
90103
export type AIChatController = {
91104
/** Open the dialog */
92105
open: () => void;
@@ -97,7 +110,10 @@ export type AIChatController = {
97110
/** Clear the conversation */
98111
clear: () => void;
99112
/** Register an event listener */
100-
on: (event: 'postMessage', listener: (input: { message: string }) => void) => () => void;
113+
on: <T extends AIChatEvent['type']>(
114+
event: T,
115+
listener: (input?: AIChatEventData<T>) => void
116+
) => () => void;
101117
};
102118

103119
const AIChatControllerContext = React.createContext<AIChatController | null>(null);
@@ -125,6 +141,17 @@ export function useAIChatState(): AIChatState {
125141
return state;
126142
}
127143

144+
function notify(
145+
listeners: AIChatEventListener[] | undefined,
146+
input: Omit<AIChatEvent, 'type'>
147+
): void {
148+
if (!listeners) return;
149+
// Defer event listeners to next tick so React can process state updates first
150+
setTimeout(() => {
151+
listeners.forEach((listener) => listener(input));
152+
}, 0);
153+
}
154+
128155
/**
129156
* Provide the controller to interact with the AI chat.
130157
*/
@@ -140,9 +167,7 @@ export function AIChatProvider(props: {
140167
const language = useLanguage();
141168

142169
// Event listeners storage
143-
const eventsRef = React.useRef<Map<'postMessage', Array<(input: { message: string }) => void>>>(
144-
new Map()
145-
);
170+
const eventsRef = React.useRef<Map<AIChatEvent['type'], AIChatEventListener[]>>(new Map());
146171

147172
// Open AI chat and sync with search state
148173
const onOpen = React.useCallback(() => {
@@ -156,6 +181,8 @@ export function AIChatProvider(props: {
156181
scope: prev?.scope ?? 'default',
157182
open: false, // Close search popover when opening chat
158183
}));
184+
185+
notify(eventsRef.current.get('open'), {});
159186
}, [setSearchState]);
160187

161188
// Close AI chat and clear ask parameter
@@ -169,6 +196,8 @@ export function AIChatProvider(props: {
169196
scope: prev?.scope ?? 'default',
170197
open: false,
171198
}));
199+
200+
notify(eventsRef.current.get('close'), {});
172201
}, [setSearchState]);
173202

174203
// Stream a message with the AI backend
@@ -386,11 +415,7 @@ export function AIChatProvider(props: {
386415
}));
387416
}
388417

389-
// Defer event listeners to next tick so React can process state updates first
390-
setTimeout(() => {
391-
const listeners = eventsRef.current.get('postMessage') || [];
392-
listeners.forEach((listener) => listener(input));
393-
}, 0);
418+
notify(eventsRef.current.get('postMessage'), { message: input.message });
394419

395420
if (query === input.message) {
396421
// Return early if the message is the same as the previous message
@@ -458,9 +483,12 @@ export function AIChatProvider(props: {
458483
}, [setSearchState]);
459484

460485
const onEvent = React.useCallback(
461-
(event: 'postMessage', listener: (input: { message: string }) => void) => {
486+
<T extends AIChatEvent['type']>(
487+
event: T,
488+
listener: (input?: AIChatEventData<T>) => void
489+
) => {
462490
const listeners = eventsRef.current.get(event) || [];
463-
listeners.push(listener);
491+
listeners.push(listener as AIChatEventListener);
464492
eventsRef.current.set(event, listeners);
465493
return () => {
466494
const currentListeners = eventsRef.current.get(event) || [];

packages/gitbook/src/components/Embeddable/EmbeddableIframeAPI.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function EmbeddableIframeAPI(props: {
4040
}, [baseURL, siteTitle]);
4141

4242
React.useEffect(() => {
43-
return chatController.on('postMessage', () => {
43+
return chatController.on('open', () => {
4444
router.push(`${baseURL}/assistant`);
4545
});
4646
}, [router, baseURL, chatController]);
@@ -154,15 +154,6 @@ export function EmbeddableIframeTabs(props: { active?: string }) {
154154

155155
const router = useRouter();
156156

157-
// Override the active tab if it doesn't match the configured tabs
158-
React.useEffect(() => {
159-
if (active === 'assistant' && !configuredTabs.includes('assistant')) {
160-
router.replace(`${baseURL}/page`);
161-
} else if (active === 'docs' && !configuredTabs.includes('docs')) {
162-
router.replace(`${baseURL}/assistant`);
163-
}
164-
}, [configuredTabs, baseURL, router, active]);
165-
166157
const tabs = [
167158
config.aiMode === CustomizationAIMode.Assistant &&
168159
assistants[0] &&
@@ -188,6 +179,21 @@ export function EmbeddableIframeTabs(props: { active?: string }) {
188179
: null,
189180
].filter((tab) => tab !== null);
190181

182+
// Override the active tab if it doesn't match the configured tabs
183+
React.useEffect(() => {
184+
const hasAssistant = tabs.find((tab) => tab.key === 'assistant');
185+
const hasDocs = tabs.find((tab) => tab.key === 'docs');
186+
if (!hasAssistant && !hasDocs) {
187+
// No valid tabs, do not redirect
188+
return;
189+
}
190+
if (active === 'assistant' && !hasAssistant) {
191+
router.replace(`${baseURL}/page`);
192+
} else if (active === 'docs' && !hasDocs) {
193+
router.replace(`${baseURL}/assistant`);
194+
}
195+
}, [tabs, baseURL, router, active]);
196+
191197
return (
192198
<>
193199
{tabs.length > 1 || actions.length > 0

packages/gitbook/src/lib/embeddable.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export function getEmbeddableLinker(linker: GitBookLinker): GitBookLinker {
5959
toLinkForContent(rawURL: string): string {
6060
const result = linker.toLinkForContent(rawURL);
6161
// If the link is not relative or already an embed, return it as is
62-
if (result.includes('~gitbook') || !result.startsWith('/')) {
62+
if (result.includes('~gitbook/embed') || !result.startsWith('/')) {
6363
return result;
6464
}
6565

0 commit comments

Comments
 (0)