mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-24 11:26:31 +00:00
Fix inconsistent state between q= and ask= and keyboard navigation (#3519)
This commit is contained in:
@@ -31,6 +31,11 @@ export type AIChatState = {
|
||||
*/
|
||||
query: string | null;
|
||||
|
||||
/**
|
||||
* The first query sent to the AI. This is appended to the URL when the AI chat is opened.
|
||||
*/
|
||||
initialQuery: string | null;
|
||||
|
||||
/**
|
||||
* Messages in the session.
|
||||
*/
|
||||
@@ -79,6 +84,7 @@ const globalState = zustand.create<{
|
||||
followUpSuggestions: [],
|
||||
loading: false,
|
||||
error: false,
|
||||
initialQuery: null,
|
||||
},
|
||||
setState: (fn) => set((state) => ({ state: { ...state.state, ...fn(state.state) } })),
|
||||
};
|
||||
@@ -102,17 +108,14 @@ export function useAIChatController(): AIChatController {
|
||||
const trackEvent = useTrackEvent();
|
||||
const [searchState, setSearchState] = useSearch(true);
|
||||
|
||||
// Track if we've initialized from the URL ask parameter
|
||||
const hasInitializedFromUrlRef = React.useRef<boolean>(false);
|
||||
|
||||
// Open AI chat and sync with search state
|
||||
const onOpen = React.useCallback(() => {
|
||||
const { messages } = globalState.getState().state;
|
||||
const { initialQuery } = globalState.getState().state;
|
||||
setState((state) => ({ ...state, opened: true }));
|
||||
|
||||
// Update search state to show ask mode with first message or current ask value
|
||||
setSearchState((prev) => ({
|
||||
ask: prev?.ask ?? messages[0]?.query ?? '',
|
||||
ask: prev?.ask ?? initialQuery ?? '',
|
||||
query: prev?.query ?? null,
|
||||
global: prev?.global ?? false,
|
||||
open: false, // Close search popover when opening chat
|
||||
@@ -249,11 +252,9 @@ export function useAIChatController(): AIChatController {
|
||||
followUpSuggestions: [],
|
||||
responseId: null,
|
||||
error: false,
|
||||
initialQuery: null,
|
||||
}));
|
||||
|
||||
// Reset initialization flag so URL ask can be processed again
|
||||
hasInitializedFromUrlRef.current = false;
|
||||
|
||||
// Reset ask parameter to empty string (keeps chat open but clears content)
|
||||
setSearchState((prev) => ({
|
||||
ask: '',
|
||||
@@ -277,18 +278,21 @@ export function useAIChatController(): AIChatController {
|
||||
|
||||
// Auto-post the message if ask has content
|
||||
if (searchState?.ask?.trim()) {
|
||||
const trimmedAsk = searchState.ask.trim();
|
||||
const { loading, initialQuery } = globalState.getState().state;
|
||||
|
||||
// Don't trigger if we're already posting a message
|
||||
const loading = globalState.getState().state.loading;
|
||||
if (loading) return;
|
||||
|
||||
// Only initialize once from URL
|
||||
if (hasInitializedFromUrlRef.current) return;
|
||||
// Only initialize once per URL ask value
|
||||
if (initialQuery === trimmedAsk) return;
|
||||
|
||||
// Wait for messageContextRef to be defined before proceeding
|
||||
if (!messageContextRef.current?.location) return;
|
||||
|
||||
hasInitializedFromUrlRef.current = true;
|
||||
onPostMessage({ message: searchState.ask.trim() });
|
||||
// Mark this ask value as processed
|
||||
setState((state) => ({ ...state, initialQuery: trimmedAsk }));
|
||||
onPostMessage({ message: trimmedAsk });
|
||||
}
|
||||
}, [
|
||||
searchState?.ask,
|
||||
@@ -296,6 +300,7 @@ export function useAIChatController(): AIChatController {
|
||||
searchState?.open,
|
||||
messageContextRef,
|
||||
onOpen,
|
||||
setState,
|
||||
onPostMessage,
|
||||
]);
|
||||
|
||||
|
||||
@@ -93,6 +93,15 @@ export function AIChatWindow(props: {
|
||||
block: 'start',
|
||||
});
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
if (lastUserMessageRef.current) {
|
||||
lastUserMessageRef.current.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// We want the chat messages to scroll underneath the input, but they should scroll past the input when scrolling all the way down.
|
||||
// The best way to do this is to observe the input height and adjust the padding bottom of the scroll container accordingly.
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
@@ -103,7 +112,10 @@ export function AIChatWindow(props: {
|
||||
if (inputRef.current) {
|
||||
observer.observe(inputRef.current);
|
||||
}
|
||||
return () => observer.disconnect();
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -38,7 +38,7 @@ export function AIChatInput(props: {
|
||||
// This fixes inconsistent focus behaviour across browsers
|
||||
const timeout = setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 50);
|
||||
}, 150);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
|
||||
@@ -54,10 +54,12 @@ export function SearchContainer(props: SearchContainerProps) {
|
||||
|
||||
const onClose = React.useCallback(
|
||||
async (to?: string) => {
|
||||
if (state?.query === '') {
|
||||
await setSearchState(null);
|
||||
} else if (state) {
|
||||
await setSearchState({ ...state, open: false });
|
||||
if (state) {
|
||||
await setSearchState({
|
||||
...state,
|
||||
open: false,
|
||||
query: state.query === '' ? null : state.query,
|
||||
});
|
||||
}
|
||||
|
||||
if (to) {
|
||||
@@ -131,6 +133,8 @@ export function SearchContainer(props: SearchContainerProps) {
|
||||
const normalizedQuery = state?.query?.trim() ?? '';
|
||||
const normalizedAsk = state?.ask?.trim() ?? '';
|
||||
|
||||
const showAsk = aiMode === CustomizationAIMode.Search && normalizedAsk;
|
||||
|
||||
return (
|
||||
<SearchAskProvider value={searchAsk}>
|
||||
<Popover
|
||||
@@ -138,10 +142,10 @@ export function SearchContainer(props: SearchContainerProps) {
|
||||
// Only show content if there's a query or Ask is enabled
|
||||
(state?.query || aiMode !== CustomizationAIMode.None) && open ? (
|
||||
<React.Suspense fallback={null}>
|
||||
{isMultiVariants && !state?.ask ? (
|
||||
{isMultiVariants && !showAsk ? (
|
||||
<SearchScopeToggle spaceTitle={spaceTitle} />
|
||||
) : null}
|
||||
{state !== null && !state.ask ? (
|
||||
{state !== null && !showAsk ? (
|
||||
<SearchResults
|
||||
ref={resultsRef}
|
||||
query={normalizedQuery}
|
||||
@@ -150,7 +154,7 @@ export function SearchContainer(props: SearchContainerProps) {
|
||||
spaceId={spaceId}
|
||||
/>
|
||||
) : null}
|
||||
{normalizedAsk ? <SearchAskAnswer query={normalizedAsk} /> : null}
|
||||
{showAsk ? <SearchAskAnswer query={normalizedAsk} /> : null}
|
||||
</React.Suspense>
|
||||
) : null
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ export const de = {
|
||||
ai_chat_context_info_provided_by_the_site: 'Von der Website bereitgestellte Informationen',
|
||||
ai_chat_context_previous_messages: 'Vorherige Nachrichten',
|
||||
ai_chat_context_disclaimer: 'KI-Antworten können Fehler enthalten.',
|
||||
ai_chat_input_placeholder: 'Fragen, suchen oder Aktion ausführen...',
|
||||
ai_chat_input_placeholder: 'Fragen, suchen oder erklären...',
|
||||
send: 'Senden',
|
||||
actions: 'Aktionen',
|
||||
ai_chat_suggested_questions_title: 'Vorgeschlagene Fragen',
|
||||
|
||||
@@ -85,7 +85,7 @@ export const en = {
|
||||
ai_chat_context_info_provided_by_the_site: 'Info provided by the site',
|
||||
ai_chat_context_previous_messages: 'Previous messages',
|
||||
ai_chat_context_disclaimer: 'AI responses may contain mistakes.',
|
||||
ai_chat_input_placeholder: 'Ask, search, or take action...',
|
||||
ai_chat_input_placeholder: 'Ask, search, or explain...',
|
||||
send: 'Send',
|
||||
actions: 'Actions',
|
||||
ai_chat_suggested_questions_title: 'Suggested questions',
|
||||
|
||||
@@ -89,7 +89,7 @@ export const es: TranslationLanguage = {
|
||||
ai_chat_context_info_provided_by_the_site: 'Información proporcionada por el sitio',
|
||||
ai_chat_context_previous_messages: 'Mensajes anteriores',
|
||||
ai_chat_context_disclaimer: 'Las respuestas de IA pueden contener errores.',
|
||||
ai_chat_input_placeholder: 'Pregunta, busca o realiza una acción...',
|
||||
ai_chat_input_placeholder: 'Pregunta, busca o explica...',
|
||||
send: 'Enviar',
|
||||
actions: 'Acciones',
|
||||
ai_chat_suggested_questions_title: 'Preguntas sugeridas',
|
||||
|
||||
@@ -84,7 +84,7 @@ export const fr = {
|
||||
ai_chat_context_info_provided_by_the_site: 'Informations fournies par le site',
|
||||
ai_chat_context_previous_messages: 'Messages précédents',
|
||||
ai_chat_context_disclaimer: 'Les réponses générées peuvent contenir des erreurs.',
|
||||
ai_chat_input_placeholder: 'Rechercher…',
|
||||
ai_chat_input_placeholder: 'Demander, rechercher ou expliquer...',
|
||||
send: 'Envoyer',
|
||||
actions: 'Actions',
|
||||
ai_chat_suggested_questions_title: 'Suggestions de questions',
|
||||
|
||||
@@ -87,7 +87,7 @@ export const ja: TranslationLanguage = {
|
||||
ai_chat_context_info_provided_by_the_site: 'サイトから提供された情報',
|
||||
ai_chat_context_previous_messages: '以前のメッセージ',
|
||||
ai_chat_context_disclaimer: 'AIの回答には誤りが含まれる場合があります。',
|
||||
ai_chat_input_placeholder: '質問、検索、またはアクションを実行...',
|
||||
ai_chat_input_placeholder: '質問、検索、または説明...',
|
||||
send: '送信',
|
||||
actions: 'アクション',
|
||||
ai_chat_suggested_questions_title: 'おすすめの質問',
|
||||
|
||||
@@ -87,7 +87,7 @@ export const nl: TranslationLanguage = {
|
||||
ai_chat_context_info_provided_by_the_site: 'Informatie verstrekt door de site',
|
||||
ai_chat_context_previous_messages: 'Vorige berichten',
|
||||
ai_chat_context_disclaimer: 'AI-antwoorden kunnen fouten bevatten.',
|
||||
ai_chat_input_placeholder: 'Vraag, zoek of voer een actie uit...',
|
||||
ai_chat_input_placeholder: 'Vraag, zoek of leg uit...',
|
||||
send: 'Versturen',
|
||||
actions: 'Acties',
|
||||
ai_chat_suggested_questions_title: 'Voorgestelde vragen',
|
||||
|
||||
@@ -88,7 +88,7 @@ export const no: TranslationLanguage = {
|
||||
ai_chat_context_info_provided_by_the_site: 'Informasjon gitt av nettstedet',
|
||||
ai_chat_context_previous_messages: 'Tidligere meldinger',
|
||||
ai_chat_context_disclaimer: 'AI-svar kan inneholde feil.',
|
||||
ai_chat_input_placeholder: 'Spør, søk eller utfør en handling...',
|
||||
ai_chat_input_placeholder: 'Spør, søk eller forklar...',
|
||||
send: 'Send',
|
||||
actions: 'Handlinger',
|
||||
ai_chat_suggested_questions_title: 'Foreslåtte spørsmål',
|
||||
|
||||
@@ -87,7 +87,7 @@ export const pt_br = {
|
||||
ai_chat_context_info_provided_by_the_site: 'Informações fornecidas pelo site',
|
||||
ai_chat_context_previous_messages: 'Mensagens anteriores',
|
||||
ai_chat_context_disclaimer: 'Respostas de IA podem conter erros.',
|
||||
ai_chat_input_placeholder: 'Pergunte, pesquise ou execute uma ação...',
|
||||
ai_chat_input_placeholder: 'Pergunte, pesquise ou explique...',
|
||||
send: 'Enviar',
|
||||
actions: 'Ações',
|
||||
ai_chat_suggested_questions_title: 'Perguntas sugeridas',
|
||||
|
||||
@@ -84,7 +84,7 @@ export const zh: TranslationLanguage = {
|
||||
ai_chat_context_info_provided_by_the_site: '网站提供的信息',
|
||||
ai_chat_context_previous_messages: '之前的消息',
|
||||
ai_chat_context_disclaimer: '人工智能的回答可能包含错误。',
|
||||
ai_chat_input_placeholder: '询问、搜索或执行操作...',
|
||||
ai_chat_input_placeholder: '询问、搜索或解释...',
|
||||
send: '发送',
|
||||
actions: '操作',
|
||||
ai_chat_suggested_questions_title: '建议的问题',
|
||||
|
||||
Reference in New Issue
Block a user