From 34dceb2f9735f0a7820bc6c43be6b9800883db42 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Mon, 12 May 2025 13:22:12 +0200 Subject: [PATCH] Toggleable layout --- .../src/components/Search/SearchAskAnswer.tsx | 32 +++++---- .../src/components/Search/SearchModal.tsx | 70 +++++++++++++++---- .../src/components/Search/SearchResults.tsx | 28 ++++---- .../src/components/Search/useSearch.ts | 26 ++++--- 4 files changed, 104 insertions(+), 52 deletions(-) diff --git a/packages/gitbook/src/components/Search/SearchAskAnswer.tsx b/packages/gitbook/src/components/Search/SearchAskAnswer.tsx index 8b0c80393..7e8f83bd2 100644 --- a/packages/gitbook/src/components/Search/SearchAskAnswer.tsx +++ b/packages/gitbook/src/components/Search/SearchAskAnswer.tsx @@ -1,21 +1,19 @@ 'use client'; -import { Icon } from '@gitbook/icons'; -import { readStreamableValue } from 'ai/rsc'; -import React from 'react'; - -import { Loading } from '@/components/primitives'; import { useLanguage } from '@/intl/client'; import { t } from '@/intl/translate'; import type { TranslationLanguage } from '@/intl/translations'; import { tcls } from '@/lib/tailwind'; +import { Icon } from '@gitbook/icons'; +import { readStreamableValue } from 'ai/rsc'; +import React from 'react'; +import { motion } from 'framer-motion'; import { useTrackEvent } from '../Insights'; import { Link } from '../primitives'; import { useSearchAskContext } from './SearchAskContext'; import { type AskAnswerResult, type AskAnswerSource, streamAskQuestion } from './server-actions'; import { useSearch, useSearchLink } from './useSearch'; - export type SearchAskState = | { type: 'answer'; @@ -88,13 +86,22 @@ export function SearchAskAnswer(props: { query: string }) { }, [setAskState]); const loading = ( -
- +
+ {[...Array(9)].map((_, index) => ( +
+ ))}
); return ( - <> + {askState?.type === 'answer' ? ( @@ -104,7 +111,7 @@ export function SearchAskAnswer(props: { query: string }) {
{t(language, 'search_ask_error')}
) : null} {askState?.type === 'loading' ? loading : null} - +
); } @@ -138,10 +145,7 @@ function AnswerBody(props: { answer: AskAnswerResult }) { return ( <> -
+
{answer.body ?? t(language, 'search_ask_no_answer')} {answer.followupQuestions.length > 0 ? ( diff --git a/packages/gitbook/src/components/Search/SearchModal.tsx b/packages/gitbook/src/components/Search/SearchModal.tsx index 6f072e5ea..93ae7b791 100644 --- a/packages/gitbook/src/components/Search/SearchModal.tsx +++ b/packages/gitbook/src/components/Search/SearchModal.tsx @@ -7,6 +7,7 @@ import { useHotkeys } from 'react-hotkeys-hook'; import { tString, useLanguage } from '@/intl/client'; import { tcls } from '@/lib/tailwind'; +import { Button } from '../primitives/Button'; import { LoadingPane } from '../primitives/LoadingPane'; import { SearchAskAnswer } from './SearchAskAnswer'; import { SearchAskProvider, useSearchAskState } from './SearchAskContext'; @@ -33,7 +34,7 @@ export function SearchModal(props: SearchModalProps) { 'mod+k', (e) => { e.preventDefault(); - setSearchState({ ask: false, query: '', global: false }); + setSearchState({ mode: 'both', query: '', global: false }); }, [] ); @@ -177,7 +178,7 @@ function SearchModalBody( const onChange = (event: React.ChangeEvent) => { setSearchState({ - ask: false, // When typing, we go back to the default search mode + mode: 'both', // When typing, we go back to the default search mode query: event.target.value, global: state.global, }); @@ -288,18 +289,59 @@ function SearchModalBody(
-
- -
-
- -
+ + {state.mode !== 'chat' ? ( + + + + ) : null} + + {state.mode !== 'results' ? ( + + {state.mode === 'chat' ? ( +
); diff --git a/packages/gitbook/src/components/Search/SearchResults.tsx b/packages/gitbook/src/components/Search/SearchResults.tsx index f5195544f..8476bfe3f 100644 --- a/packages/gitbook/src/components/Search/SearchResults.tsx +++ b/packages/gitbook/src/components/Search/SearchResults.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { t, useLanguage } from '@/intl/client'; import { tcls } from '@/lib/tailwind'; +import { motion } from 'framer-motion'; import { useTrackEvent } from '../Insights'; import { Loading } from '../primitives'; import { SearchPageResultItem } from './SearchPageResultItem'; @@ -18,6 +19,7 @@ import { searchSiteSpaceContent, streamRecommendedQuestions, } from './server-actions'; +import { useSearch } from './useSearch'; export interface SearchResultsRef { moveUp(): void; @@ -61,6 +63,7 @@ export const SearchResults = React.forwardRef(function SearchResults( }>({ results: [], fetching: true }); const [cursor, setCursor] = React.useState(null); const refs = React.useRef<(null | HTMLAnchorElement)[]>([]); + const [searchState, setSearchState] = useSearch(); React.useEffect(() => { if (!query) { @@ -157,9 +160,12 @@ export const SearchResults = React.forwardRef(function SearchResults( setCursor(null); } else if (results.length > 0) { // Auto-focus the first result + setSearchState((prev) => (prev ? { ...prev, mode: 'both' } : null)); setCursor(0); + } else if (results.length === 0 && !resultsState.fetching && !searchState?.manual) { + setSearchState((prev) => (prev ? { ...prev, mode: 'chat' } : null)); } - }, [results, query]); + }, [results, query, setSearchState, resultsState.fetching, searchState?.manual]); // Scroll to the active result. React.useEffect(() => { @@ -210,9 +216,12 @@ export const SearchResults = React.forwardRef(function SearchResults( if (resultsState.fetching) { return ( -
+ -
+ ); } @@ -293,16 +302,3 @@ export const SearchResults = React.forwardRef(function SearchResults( ); }); - -/** - * Add a "Ask " item at the top of the results list. - */ -function withQuestionResult(results: ResultType[], query: string): ResultType[] { - const without = results.filter((result) => result.type !== 'question'); - - if (query.length === 0) { - return without; - } - - return [{ type: 'question', id: 'question', query }, ...(without ?? [])]; -} diff --git a/packages/gitbook/src/components/Search/useSearch.ts b/packages/gitbook/src/components/Search/useSearch.ts index 8f3ecfaec..d4e20b3d4 100644 --- a/packages/gitbook/src/components/Search/useSearch.ts +++ b/packages/gitbook/src/components/Search/useSearch.ts @@ -1,19 +1,21 @@ -import { parseAsBoolean, parseAsString, useQueryStates } from 'nuqs'; +import { parseAsBoolean, parseAsString, parseAsStringEnum, useQueryStates } from 'nuqs'; import React from 'react'; import type { LinkProps } from '../primitives'; export interface SearchState { query: string; - ask: boolean; global: boolean; + mode: 'results' | 'chat' | 'both'; + manual?: boolean; } // KeyMap needs to be statically defined to avoid `setRawState` being redefined on every render. const keyMap = { q: parseAsString, - ask: parseAsBoolean, + mode: parseAsStringEnum(['both', 'results', 'chat']).withDefault('both'), global: parseAsBoolean, + manual: parseAsBoolean, }; export type UpdateSearchState = ( @@ -33,7 +35,12 @@ export function useSearch(): [SearchState | null, UpdateSearchState] { return null; } - return { query: rawState.q, ask: !!rawState.ask, global: !!rawState.global }; + return { + query: rawState.q, + mode: rawState.mode, + global: !!rawState.global, + manual: !!rawState.manual, + }; }, [rawState]); const stateRef = React.useRef(state); @@ -52,14 +59,16 @@ export function useSearch(): [SearchState | null, UpdateSearchState] { if (update === null) { return setRawState({ q: null, - ask: null, + mode: null, global: null, + manual: null, }); } return setRawState({ q: update.query, - ask: update.ask ? true : null, + mode: update.mode, global: update.global ? true : null, + manual: update.manual ? true : null, }); }, [setRawState] @@ -78,8 +87,9 @@ export function useSearchLink(): (query: Partial) => LinkProps { (query) => { const searchParams = new URLSearchParams(); searchParams.set('q', query.query ?? ''); - query.ask ? searchParams.set('ask', 'on') : searchParams.delete('ask'); + query.mode ? searchParams.set('mode', query.mode) : searchParams.delete('mode'); query.global ? searchParams.set('global', 'on') : searchParams.delete('global'); + searchParams.delete('manual'); return { href: `?${searchParams.toString()}`, prefetch: false, @@ -87,7 +97,7 @@ export function useSearchLink(): (query: Partial) => LinkProps { event.preventDefault(); setSearch((prev) => ({ query: '', - ask: false, + mode: 'both', global: false, ...(prev ?? {}), ...query,