diff --git a/.changeset/embed-host-actions.md b/.changeset/embed-host-actions.md new file mode 100644 index 000000000..e7c937a6e --- /dev/null +++ b/.changeset/embed-host-actions.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Fix host action buttons in the Docs Embed not reaching the assistant. diff --git a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/demo/route.ts b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/demo/route.ts index 43330cc1c..483d0bc60 100644 --- a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/demo/route.ts +++ b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/demo/route.ts @@ -127,7 +127,15 @@ export async function GET( 'Show me tips and tricks', ], tabs: ['assistant', 'search', 'docs'], - closeButton: useCustomTrigger + closeButton: useCustomTrigger, + // Host-page action: posts a message into the chat, like an embedding site would. + actions: [ + { + icon: 'rocket', + label: 'Ask about pricing', + onClick: () => window.GitBook('postUserMessage', 'How much does GitBook cost?'), + }, + ], }); if (useCustomTrigger) { diff --git a/packages/gitbook/src/components/AI/useAIChat.tsx b/packages/gitbook/src/components/AI/useAIChat.tsx index 61a233b80..36cfb8224 100644 --- a/packages/gitbook/src/components/AI/useAIChat.tsx +++ b/packages/gitbook/src/components/AI/useAIChat.tsx @@ -14,7 +14,7 @@ import { } from '@gitbook/api'; import { getInsightsSession, useTrackEvent } from '../Insights'; -import { useSetSearchState } from '../Search'; +import { type UpdateSearchState, useSetSearchState } from '../Search'; import { addRecentSearchQuery } from '../Search/recent-queries'; import type { AnyAIControl } from './controls'; import { ConfirmControlDef, ConfirmControlOutputSchema } from './controls/ConfirmControl'; @@ -32,6 +32,8 @@ import { useCurrentContent } from '@/components/hooks'; import { useLanguage } from '@/intl/client'; import { tString } from '@/intl/translate'; +const noopSetSearchState: UpdateSearchState = () => Promise.resolve(new URLSearchParams()); + export type AIChatMessage = { role: AIMessageRole; content: React.ReactNode; @@ -235,7 +237,7 @@ export function AIChatProvider(props: { const messageContextRef = useAIMessageContextRef(); const trackEvent = useTrackEvent(); - const setSearchState = useSetSearchState(); + const setSearchStateInURL = useSetSearchState(); const { siteSpaceId } = useCurrentContent(); const language = useLanguage(); @@ -243,6 +245,11 @@ export function AIChatProvider(props: { ? SiteInsightsDisplayContext.Embed : SiteInsightsDisplayContext.Site; + // The embed keeps its state in its own routes, not in URL search params. + const setSearchState = renderMessageOptions?.asEmbeddable + ? noopSetSearchState + : setSearchStateInURL; + // The assistant response the user is reacting to. Snapshotted when a new user turn begins // (before it overwrites the store's responseId/query), so the self-feedback tool rates that // previous response rather than the one this reaction turn produces. @@ -610,14 +617,12 @@ export function AIChatProvider(props: { // Post a message to the AI chat const onPostMessage = React.useCallback( async (input: { message: string }) => { - const { query, messages, control, references, responding } = globalState.getState(); + const { query, messages, control, references, responding, opened } = + globalState.getState(); - if (control) { - throw new Error("We can't post a message when a control is active"); - } - - // Still streaming: queue this follow-up instead of dropping it (flushed in order in `streamResponse`). - if (responding) { + // Still streaming, or waiting on a control (e.g. a tool confirmation): queue this + // follow-up instead of dropping it (flushed in order in `streamResponse`). + if (responding || control) { globalState.setState((state) => ({ ...state, queuedMessages: [...state.queuedMessages, input.message], @@ -643,9 +648,7 @@ export function AIChatProvider(props: { notify(eventsRef.current.get('postMessage'), { message: input.message }); - if (query === input.message && references.length === 0) { - // Return early if the message is the same as the previous message - // (unless new references are staged, which change the payload) + if (query === input.message && references.length === 0 && !opened) { globalState.setState((state) => ({ ...state, opened: true, diff --git a/packages/gitbook/src/components/Embeddable/EmbeddableIframeAPI.tsx b/packages/gitbook/src/components/Embeddable/EmbeddableIframeAPI.tsx index ee1d2c5f3..6df5958e4 100644 --- a/packages/gitbook/src/components/Embeddable/EmbeddableIframeAPI.tsx +++ b/packages/gitbook/src/components/Embeddable/EmbeddableIframeAPI.tsx @@ -74,6 +74,9 @@ export function EmbeddableIframeAPI(props: { baseURL: string }) { break; } case 'postUserMessage': { + // The answer renders in the assistant, so bring it up if another tab is showing. + navToken.current++; + router.push(`${baseURL}/assistant`); chatController.postMessage({ message: message.message, }); diff --git a/packages/gitbook/src/components/PageBody/PageFooterNavigation.tsx b/packages/gitbook/src/components/PageBody/PageFooterNavigation.tsx index e50894d9a..d8b88fd0e 100644 --- a/packages/gitbook/src/components/PageBody/PageFooterNavigation.tsx +++ b/packages/gitbook/src/components/PageBody/PageFooterNavigation.tsx @@ -99,7 +99,7 @@ function NavigationCard( > {label} - + {title}