Compare commits

...

2 Commits

Author SHA1 Message Date
Zeno Kapitein 63971b9803 Update SearchContainer.tsx 2025-08-20 09:23:50 +02:00
Zeno Kapitein 64455b55e4 Add ready handshake to integration assistants 2025-08-20 09:01:54 +02:00
4 changed files with 57 additions and 8 deletions
+17 -2
View File
@@ -3,7 +3,7 @@ import type { IconName } from '@gitbook/icons';
export type GitBookIntegrationEvent = 'load' | 'unload';
export type GitBookIntegrationEventCallback = (...args: any[]) => void;
export type GitBookIntegrationEventCallback = (...args: unknown[]) => void;
export type GitBookIntegrationTool = AIToolDefinition & {
/**
@@ -70,8 +70,10 @@ export type GitBookGlobal = {
/**
* Register a custom assistant to be available on the site.
* Returns a registration object that lets the integration signal readiness
* and dispose the assistant when no longer needed.
*/
registerAssistant: (assistant: GitBookAssistant) => () => void;
registerAssistant: (assistant: GitBookAssistant) => GitBookAssistantRegistration;
};
declare global {
@@ -82,3 +84,16 @@ declare global {
GitBook?: GitBookGlobal;
}
}
export type GitBookAssistantRegistration = ((this: void) => void) & {
/**
* Call this when the integration has finished initializing and is ready to
* receive `open` calls.
*/
ready: () => void;
/**
* Unregister the assistant.
*/
dispose: () => void;
};
@@ -38,6 +38,12 @@ export type Assistant = Omit<GitBookAssistant, 'icon'> & {
* Icon of the assistant displayed in the UI.
*/
icon: ReactNode;
/**
* Assistant readiness. Built-in assistants are always ready.
* Integrations set this to true via the registration's ready() callback.
*/
ready?: boolean;
};
const AIContext = React.createContext<AIConfig | null>(null);
@@ -92,6 +98,7 @@ export function useAI(): AIContext {
},
ui: true,
mode: 'sidebar',
ready: true,
});
} else if (config.aiMode === CustomizationAIMode.Search) {
assistants.push({
@@ -116,6 +123,7 @@ export function useAI(): AIContext {
},
ui: false,
mode: 'search',
ready: true,
});
}
@@ -47,17 +47,39 @@ if (typeof window !== 'undefined') {
},
registerAssistant: (assistant) => {
const id = window.crypto.randomUUID();
// Add assistant with ready=false until the integration signals readiness
integrationAssistants.setState(
(state) => [
...state,
{ ...assistant, id, ui: assistant.ui ?? true, mode: 'overlay' },
{ ...assistant, id, ui: assistant.ui ?? true, mode: 'overlay', ready: false },
],
true
);
return () => {
integrationAssistants.setState((state) => state.filter((a) => a.id !== id), true);
const ready = () => {
integrationAssistants.setState(
(state) =>
state.map((assistant) =>
assistant.id === id ? { ...assistant, ready: true } : assistant
),
true
);
};
const dispose = () => {
integrationAssistants.setState(
(state) => state.filter((assistant) => assistant.id !== id),
true
);
};
const registration = (() => dispose()) as ((this: void) => void) & {
ready: () => void;
dispose: () => void;
};
registration.ready = ready;
registration.dispose = dispose;
return registration;
},
};
window.GitBook = gitbookGlobal;
@@ -83,6 +105,6 @@ export function LoadIntegrations() {
/**
* Client function to dispatch a GitBook event.
*/
function dispatchGitBookIntegrationEvent(type: GitBookIntegrationEvent, ...args: any[]) {
function dispatchGitBookIntegrationEvent(type: GitBookIntegrationEvent, ...args: unknown[]) {
events.get(type)?.forEach((handler) => handler(...args));
}
@@ -52,11 +52,15 @@ export function SearchContainer(props: SearchContainerProps) {
if (assistants.length === 0) return;
if (state?.ask === undefined || state?.ask === null) return;
// Wait for the first assistant to be ready (built-ins are always ready)
const first = assistants[0];
if (first?.ready === false) return;
initialRef.current = true;
// For simplicity we're only triggering the first assistant
assistants[0].open(state?.ask ?? undefined);
}, [state?.ask, assistants.length, assistants[0]?.open]);
first.open(state?.ask ?? undefined);
}, [state?.ask, assistants.length, assistants, assistants[0]?.ready]);
const onClose = React.useCallback(
async (to?: string) => {