diff --git a/bun.lockb b/bun.lockb
index dc1b41c81..55de3ded1 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json
index 47f101409..f359a16ae 100644
--- a/packages/gitbook/package.json
+++ b/packages/gitbook/package.json
@@ -43,7 +43,7 @@
"memoizee": "^0.4.15",
"next": "14.2.15",
"next-themes": "^0.2.1",
- "nuqs": "^1.17.4",
+ "nuqs": "^2.2.3",
"object-hash": "^3.0.0",
"openapi-types": "^12.1.3",
"p-map": "^7.0.0",
diff --git a/packages/gitbook/src/app/(site)/(content)/layout.tsx b/packages/gitbook/src/app/(site)/(content)/layout.tsx
index 8018aa13f..363472233 100644
--- a/packages/gitbook/src/app/(site)/(content)/layout.tsx
+++ b/packages/gitbook/src/app/(site)/(content)/layout.tsx
@@ -1,6 +1,7 @@
import { CustomizationThemeMode } from '@gitbook/api';
import { Metadata, Viewport } from 'next';
import { headers } from 'next/headers';
+import { NuqsAdapter } from 'nuqs/adapters/next/app';
import React from 'react';
import * as ReactDOM from 'react-dom';
@@ -55,46 +56,48 @@ export default async function ContentLayout(props: { children: React.ReactNode }
});
return (
-
-
+
- {children}
-
+
+ {children}
+
- {scripts.length > 0 ? (
- <>
-
- {scripts.map(({ script }) => (
-
- ))}
- >
- ) : null}
+ {scripts.length > 0 ? (
+ <>
+
+ {scripts.map(({ script }) => (
+
+ ))}
+ >
+ ) : null}
- {scripts.some((script) => script.cookies) || customization.privacyPolicy.url ? (
-
-
-
- ) : null}
+ {scripts.some((script) => script.cookies) || customization.privacyPolicy.url ? (
+
+
+
+ ) : null}
-
+
-
-
+
+
+
);
}
diff --git a/packages/gitbook/src/components/Search/SearchAskAnswer.tsx b/packages/gitbook/src/components/Search/SearchAskAnswer.tsx
index 71b01c228..af65a6897 100644
--- a/packages/gitbook/src/components/Search/SearchAskAnswer.tsx
+++ b/packages/gitbook/src/components/Search/SearchAskAnswer.tsx
@@ -51,42 +51,29 @@ export function SearchAskAnswer(props: { pointer: SiteContentPointer; query: str
React.useEffect(() => {
let cancelled = false;
- setState({
- type: 'loading',
- });
+ setState({ type: 'loading' });
(async () => {
- const stream = iterateStreamResponse(
- streamAskQuestion(organizationId, siteId, siteSpaceId ?? null, query),
- );
+ const response = streamAskQuestion(organizationId, siteId, siteSpaceId ?? null, query);
+ const stream = iterateStreamResponse(response);
- setSearchState((prev) =>
- prev
- ? {
- ...prev,
- ask: true,
- query,
- }
- : null,
- );
+ // When we pass in "ask" mode, the query could still be updated by the client
+ // we ensure that the query is up-to-date before starting the stream.
+ setSearchState((prev) => (prev ? { ...prev, query, ask: true } : null));
for await (const chunk of stream) {
if (cancelled) {
return;
}
- setState({
- type: 'answer',
- answer: chunk,
- });
+
+ setState({ type: 'answer', answer: chunk });
}
- })().catch((error) => {
+ })().catch(() => {
if (cancelled) {
return;
}
- setState({
- type: 'error',
- });
+ setState({ type: 'error' });
});
return () => {
@@ -96,7 +83,7 @@ export function SearchAskAnswer(props: { pointer: SiteContentPointer; query: str
cancelled = true;
}
};
- }, [organizationId, siteId, siteSpaceId, query]);
+ }, [organizationId, siteId, siteSpaceId, query, setState, setSearchState]);
React.useEffect(() => {
return () => {
diff --git a/packages/gitbook/src/components/Search/SearchModal.tsx b/packages/gitbook/src/components/Search/SearchModal.tsx
index 1e74d6a9d..124342750 100644
--- a/packages/gitbook/src/components/Search/SearchModal.tsx
+++ b/packages/gitbook/src/components/Search/SearchModal.tsx
@@ -14,7 +14,7 @@ import { tcls } from '@/lib/tailwind';
import { SearchAskAnswer, searchAskState } from './SearchAskAnswer';
import { SearchResults, SearchResultsRef } from './SearchResults';
import { SearchScopeToggle } from './SearchScopeToggle';
-import { SearchState, useSearch } from './useSearch';
+import { SearchState, UpdateSearchState, useSearch } from './useSearch';
import { LoadingPane } from '../primitives/LoadingPane';
interface SearchModalProps {
@@ -55,10 +55,6 @@ export function SearchModal(props: SearchModalProps) {
};
}, [isSearchOpened]);
- const onChangeQuery = (newQuery: SearchState) => {
- setSearchState(newQuery);
- };
-
const onClose = async (to?: string) => {
await setSearchState(null);
if (to) {
@@ -129,7 +125,7 @@ export function SearchModal(props: SearchModalProps) {
@@ -142,7 +138,7 @@ export function SearchModal(props: SearchModalProps) {
function SearchModalBody(
props: SearchModalProps & {
state: SearchState;
- onChangeQuery: (newQuery: SearchState) => void;
+ setSearchState: UpdateSearchState;
onClose: (to?: string) => void;
},
) {
@@ -154,7 +150,7 @@ function SearchModalBody(
withAsk,
isMultiVariants,
state,
- onChangeQuery,
+ setSearchState,
onClose,
} = props;
@@ -193,7 +189,7 @@ function SearchModalBody(
};
const onChange = (event: React.ChangeEvent) => {
- onChangeQuery({
+ setSearchState({
ask: false, // When typing, we go back to the default search mode
query: event.target.value,
global: state.global,
@@ -312,11 +308,7 @@ function SearchModalBody(
query={state.query}
withAsk={withAsk}
onSwitchToAsk={() => {
- onChangeQuery({
- ask: true,
- query: state.query,
- global: state.global,
- });
+ setSearchState((state) => (state ? { ...state, ask: true } : null));
}}
>
) : null}
diff --git a/packages/gitbook/src/components/Search/useSearch.ts b/packages/gitbook/src/components/Search/useSearch.ts
index 6301ec4f3..713a08630 100644
--- a/packages/gitbook/src/components/Search/useSearch.ts
+++ b/packages/gitbook/src/components/Search/useSearch.ts
@@ -16,18 +16,17 @@ const keyMap = {
global: parseAsBoolean,
};
-const options: UseQueryStatesOptions = {
- history: 'replace',
-};
+export type UpdateSearchState = (
+ update: React.SetStateAction,
+) => Promise;
/**
* Hook to access the current search query and update it.
*/
-export function useSearch(): [
- SearchState | null,
- (update: React.SetStateAction) => Promise,
-] {
- const [rawState, setRawState] = useQueryStates(keyMap, options);
+export function useSearch(): [SearchState | null, UpdateSearchState] {
+ const [rawState, setRawState] = useQueryStates(keyMap, {
+ history: 'replace',
+ });
const state = React.useMemo(() => {
if (rawState === null || rawState.q === null) {