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 GitBookIntegrationEvent = 'load' | 'unload';
export type GitBookIntegrationEventCallback = (...args: any[]) => void; export type GitBookIntegrationEventCallback = (...args: unknown[]) => void;
export type GitBookIntegrationTool = AIToolDefinition & { export type GitBookIntegrationTool = AIToolDefinition & {
/** /**
@@ -70,8 +70,10 @@ export type GitBookGlobal = {
/** /**
* Register a custom assistant to be available on the site. * 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 { declare global {
@@ -82,3 +84,16 @@ declare global {
GitBook?: GitBookGlobal; 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 of the assistant displayed in the UI.
*/ */
icon: ReactNode; 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); const AIContext = React.createContext<AIConfig | null>(null);
@@ -92,6 +98,7 @@ export function useAI(): AIContext {
}, },
ui: true, ui: true,
mode: 'sidebar', mode: 'sidebar',
ready: true,
}); });
} else if (config.aiMode === CustomizationAIMode.Search) { } else if (config.aiMode === CustomizationAIMode.Search) {
assistants.push({ assistants.push({
@@ -116,6 +123,7 @@ export function useAI(): AIContext {
}, },
ui: false, ui: false,
mode: 'search', mode: 'search',
ready: true,
}); });
} }
@@ -47,17 +47,39 @@ if (typeof window !== 'undefined') {
}, },
registerAssistant: (assistant) => { registerAssistant: (assistant) => {
const id = window.crypto.randomUUID(); const id = window.crypto.randomUUID();
// Add assistant with ready=false until the integration signals readiness
integrationAssistants.setState( integrationAssistants.setState(
(state) => [ (state) => [
...state, ...state,
{ ...assistant, id, ui: assistant.ui ?? true, mode: 'overlay' }, { ...assistant, id, ui: assistant.ui ?? true, mode: 'overlay', ready: false },
], ],
true true
); );
return () => { const ready = () => {
integrationAssistants.setState((state) => state.filter((a) => a.id !== id), true); 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; window.GitBook = gitbookGlobal;
@@ -83,6 +105,6 @@ export function LoadIntegrations() {
/** /**
* Client function to dispatch a GitBook event. * 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)); events.get(type)?.forEach((handler) => handler(...args));
} }
@@ -52,11 +52,15 @@ export function SearchContainer(props: SearchContainerProps) {
if (assistants.length === 0) return; if (assistants.length === 0) return;
if (state?.ask === undefined || state?.ask === null) 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; initialRef.current = true;
// For simplicity we're only triggering the first assistant // For simplicity we're only triggering the first assistant
assistants[0].open(state?.ask ?? undefined); first.open(state?.ask ?? undefined);
}, [state?.ask, assistants.length, assistants[0]?.open]); }, [state?.ask, assistants.length, assistants, assistants[0]?.ready]);
const onClose = React.useCallback( const onClose = React.useCallback(
async (to?: string) => { async (to?: string) => {