Fix inconsistent state between q= and ask= and keyboard navigation (#3519)

This commit is contained in:
Zeno Kapitein
2025-07-31 13:20:27 +02:00
committed by GitHub
parent 81da82aed7
commit ea37977b1e
13 changed files with 52 additions and 31 deletions
@@ -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
}
+1 -1
View File
@@ -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',
+1 -1
View File
@@ -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',
+1 -1
View File
@@ -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',
+1 -1
View File
@@ -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',
+1 -1
View File
@@ -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: 'おすすめの質問',
+1 -1
View File
@@ -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',
+1 -1
View File
@@ -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',
+1 -1
View File
@@ -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: '建议的问题',