Skip to content

Commit 11f4241

Browse files
committed
Update layout and transitions
1 parent 5a410fe commit 11f4241

File tree

4 files changed

+193
-134
lines changed

4 files changed

+193
-134
lines changed

packages/gitbook/src/components/RootLayout/globals.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939

4040
/* Light mode */
4141
::-webkit-scrollbar {
42-
@apply bg-tint-subtle;
42+
@apply bg-tint-subtle z-50;
4343
width: 8px;
4444
height: 8px;
4545
}

packages/gitbook/src/components/Search/SearchChat.tsx

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
import { tcls } from '@/lib/tailwind';
33
import { filterOutNullable } from '@/lib/typescript';
44
import { Icon } from '@gitbook/icons';
5-
import { motion } from 'framer-motion';
6-
import { useEffect, useState } from 'react';
5+
import { useEffect, useRef, useState } from 'react';
76
import { useVisitedPages } from '../Insights/useVisitedPages';
87
import { Button } from '../primitives';
98
import { isQuestion } from './isQuestion';
109
import { streamAISearchAnswer, streamAISearchSummary } from './server-actions';
10+
import { useSearch } from './useSearch';
1111

1212
export function SearchChat(props: { query: string }) {
1313
// const currentPage = usePageContext();
@@ -18,11 +18,15 @@ export function SearchChat(props: { query: string }) {
1818
const visitedPages = useVisitedPages((state) => state.pages);
1919
const [summary, setSummary] = useState('');
2020
const [messages, setMessages] = useState<
21-
{ role: string; content?: string; fetching?: boolean }[]
21+
{ role: string; content?: string; context?: string; fetching?: boolean }[]
2222
>([]);
2323
const [followupQuestions, setFollowupQuestions] = useState<string[]>();
2424

2525
const [responseId, setResponseId] = useState<string | null>(null);
26+
const [searchState, setSearchState] = useSearch();
27+
28+
const containerRef = useRef<HTMLDivElement>(null);
29+
const latestMessageRef = useRef<HTMLDivElement>(null);
2630

2731
useEffect(() => {
2832
let cancelled = false;
@@ -55,12 +59,9 @@ export function SearchChat(props: { query: string }) {
5559

5660
if (query) {
5761
setMessages([
58-
{
59-
role: 'user',
60-
content: query,
61-
},
6262
{
6363
role: 'assistant',
64+
context: `You asked ${isQuestion(query) ? '' : 'about'} "${query}"`,
6465
fetching: true,
6566
},
6667
]);
@@ -97,8 +98,50 @@ export function SearchChat(props: { query: string }) {
9798
}
9899
}, [query, responseId]);
99100

101+
useEffect(() => {
102+
if (latestMessageRef.current) {
103+
latestMessageRef.current.scrollIntoView({
104+
behavior: 'smooth',
105+
block: 'start',
106+
});
107+
}
108+
}, [messages]);
109+
100110
return (
101-
<motion.div layout="position" className="relative mx-auto h-full p-8">
111+
<div
112+
className={tcls(
113+
'mx-auto h-full justify-between overflow-y-auto p-8',
114+
searchState?.mode === 'chat' && 'md:px-20'
115+
)}
116+
>
117+
{searchState?.mode === 'chat' ? (
118+
<div className="left-4 mb-8 md:absolute">
119+
<Button
120+
label="Show search results"
121+
variant="blank"
122+
size="small"
123+
icon="arrow-up-to-line"
124+
className="md:hidden"
125+
onClick={() => {
126+
setSearchState((state) =>
127+
state ? { ...state, mode: 'both', manual: true } : null
128+
);
129+
}}
130+
/>
131+
<Button
132+
label="Show search results"
133+
iconOnly
134+
variant="blank"
135+
icon="arrow-left-to-line"
136+
className="hidden md:block"
137+
onClick={() => {
138+
setSearchState((state) =>
139+
state ? { ...state, mode: 'both', manual: true } : null
140+
);
141+
}}
142+
/>
143+
</div>
144+
) : null}
102145
<div className="mx-auto flex w-full max-w-prose flex-col gap-4">
103146
<div>
104147
<h5 className="mb-1 flex items-center gap-1 font-semibold text-tint-subtle text-xs">
@@ -124,21 +167,24 @@ export function SearchChat(props: { query: string }) {
124167
)}
125168
</div>
126169

127-
{messages.map((message) => (
170+
{messages.map((message, index) => (
128171
<div
129172
key={message.content}
173+
ref={index === messages.length - 1 ? latestMessageRef : null}
130174
className={tcls(
131-
'flex flex-col gap-1',
132-
message.role === 'user' && 'items-end gap-1 self-end'
175+
'flex scroll-mt-20 scroll-mb-[100%] flex-col gap-1',
176+
message.role === 'user' && 'items-end gap-1 self-end',
177+
index === messages.length - 1 && 'mb-[45vh]'
133178
)}
134179
>
135180
{message.role === 'user' ? (
136181
<h5 className="flex items-center gap-1 font-semibold text-tint-subtle text-xs">
137-
You asked {isQuestion(query) ? '' : 'about'}
182+
{message.context ?? `You asked ${isQuestion(query) ? '' : 'about'}`}
138183
</h5>
139184
) : (
140185
<h5 className="flex items-center gap-1 font-semibold text-tint-subtle text-xs">
141-
<Icon icon="sparkle" className="mt-0.5 size-3" /> AI Answer
186+
<Icon icon="sparkle" className="mt-0.5 size-3" />{' '}
187+
{message.context ?? 'AI Answer'}
142188
</h5>
143189
)}
144190
{message.fetching ? (
@@ -194,11 +240,17 @@ export function SearchChat(props: { query: string }) {
194240
icon="arrow-up"
195241
size="medium"
196242
className="shrink-0"
243+
onClick={() => {
244+
setMessages((prev) => [
245+
...prev,
246+
{ role: 'user', content: 'Hello', fetching: false },
247+
]);
248+
}}
197249
/>
198250
</div>
199251
</div>
200252
</div>
201253
) : null}
202-
</motion.div>
254+
</div>
203255
);
204256
}

packages/gitbook/src/components/Search/SearchModal.tsx

Lines changed: 67 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -234,98 +234,84 @@ function SearchModalBody(
234234
event.stopPropagation();
235235
}}
236236
>
237-
<div className="grid grow grid-rows-[auto_1fr_1fr] overflow-hidden md:grid-cols-[1fr_1fr] md:grid-rows-[auto_1fr]">
237+
<div
238+
className={tcls(
239+
'flex',
240+
'flex-row',
241+
'items-start',
242+
state.query !== null ? 'border-b' : null,
243+
'border-tint-subtle',
244+
'col-span-full'
245+
)}
246+
>
238247
<div
239248
className={tcls(
249+
'w-full',
240250
'flex',
241251
'flex-row',
242-
'items-start',
243-
state.query !== null ? 'border-b' : null,
244-
'border-tint-subtle',
245-
'col-span-full'
252+
'flex-wrap',
253+
'gap-y-0',
254+
'gap-x-4',
255+
'items-end'
246256
)}
247257
>
248-
<div
258+
<input
259+
ref={inputRef}
260+
value={state.query}
261+
onKeyDown={onKeyDown}
262+
onChange={onChange}
249263
className={tcls(
250-
'w-full',
264+
'text-tint-strong',
265+
'placeholder:text-tint',
251266
'flex',
252-
'flex-row',
253-
'flex-wrap',
254-
'gap-y-0',
255-
'gap-x-4',
256-
'items-end'
267+
'resize-none',
268+
'flex-1',
269+
'py-4',
270+
'px-8',
271+
'focus:outline-none',
272+
'bg-transparent',
273+
'whitespace-pre-line'
257274
)}
258-
>
259-
<input
260-
ref={inputRef}
261-
value={state.query}
262-
onKeyDown={onKeyDown}
263-
onChange={onChange}
264-
className={tcls(
265-
'text-tint-strong',
266-
'placeholder:text-tint',
267-
'flex',
268-
'resize-none',
269-
'flex-1',
270-
'py-4',
271-
'px-8',
272-
'focus:outline-none',
273-
'bg-transparent',
274-
'whitespace-pre-line'
275-
)}
276-
placeholder={tString(
277-
language,
278-
withAsk
279-
? 'search_ask_input_placeholder'
280-
: 'search_input_placeholder'
281-
)}
282-
spellCheck="false"
283-
autoComplete="off"
284-
autoCorrect="off"
285-
/>
286-
{isMultiVariants ? <SearchScopeToggle spaceTitle={spaceTitle} /> : null}
287-
</div>
275+
placeholder={tString(
276+
language,
277+
withAsk ? 'search_ask_input_placeholder' : 'search_input_placeholder'
278+
)}
279+
spellCheck="false"
280+
autoComplete="off"
281+
autoCorrect="off"
282+
/>
283+
{isMultiVariants ? <SearchScopeToggle spaceTitle={spaceTitle} /> : null}
284+
</div>
285+
</div>
286+
<div className={tcls('flex grow flex-col overflow-hidden md:flex-row')}>
287+
<div
288+
key="results"
289+
className={tcls(
290+
'h-full w-full flex-1 overflow-y-auto transition-all duration-500 *:transition-opacity *:delay-200 *:duration-300',
291+
state.mode === 'chat' && 'flex-[0] *:opacity-0 *:delay-0'
292+
)}
293+
aria-hidden={state.mode === 'chat' ? 'true' : undefined}
294+
>
295+
<SearchResults
296+
ref={resultsRef}
297+
global={isMultiVariants && state.global}
298+
query={normalizedQuery}
299+
withAsk={withAsk}
300+
onSwitchToAsk={onSwitchToAsk}
301+
/>
288302
</div>
289303

290-
<AnimatePresence>
291-
{state.mode !== 'chat' ? (
292-
<motion.div
293-
key="results"
294-
layout
295-
className={tcls(
296-
'overflow-y-auto md:col-start-1 md:row-start-2',
297-
state.mode === 'results' && 'md:-col-end-1'
298-
)}
299-
initial={{ width: 0 }}
300-
animate={{ width: '100%' }}
301-
exit={{ width: 0 }}
302-
>
303-
<SearchResults
304-
ref={resultsRef}
305-
global={isMultiVariants && state.global}
306-
query={normalizedQuery}
307-
withAsk={withAsk}
308-
onSwitchToAsk={onSwitchToAsk}
309-
/>
310-
</motion.div>
311-
) : null}
312-
313-
{state.mode !== 'results' ? (
314-
<motion.div
315-
key="chat"
316-
layout
317-
className={tcls(
318-
'md:-col-end-1 overflow-y-auto overflow-x-hidden border-tint-subtle bg-tint-subtle max-md:border-t md:row-start-2 md:border-l',
319-
state.mode === 'chat' && 'md:col-start-1'
320-
)}
321-
initial={{ width: 0 }}
322-
animate={{ width: '100%' }}
323-
exit={{ width: 0 }}
324-
>
325-
<SearchChat query={normalizedQuery} />
326-
</motion.div>
327-
) : null}
328-
</AnimatePresence>
304+
<div
305+
key="chat"
306+
className={tcls(
307+
'relative h-full w-full flex-1 overflow-y-auto overflow-x-hidden border-tint-subtle bg-tint-subtle *:transition-opacity *:delay-200 *:duration-300',
308+
state.mode === 'results' && 'flex-[0] *:opacity-0 *:delay-0',
309+
state.mode === 'both' && 'max-md:border-t md:border-l'
310+
)}
311+
aria-hidden={state.mode === 'results' ? 'true' : undefined}
312+
>
313+
<SearchChat query={normalizedQuery} />
314+
</div>
329315
</div>
330316
</motion.div>
331317
);

0 commit comments

Comments
 (0)