Skip to content

Commit c156e94

Browse files
authored
Update Assistant styling (#3797)
1 parent 461e15f commit c156e94

File tree

26 files changed

+315
-264
lines changed

26 files changed

+315
-264
lines changed

packages/embed/src/standalone/style.css

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@
2929
--gitbook-widget-easing-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
3030
}
3131

32+
@media (prefers-color-scheme: dark) {
33+
:root {
34+
--gitbook-widget-text-color: #FFFFFF;
35+
--gitbook-widget-border-color: #202020;
36+
--gitbook-widget-background-translucent: rgba(15, 15, 15, 0.9);
37+
--gitbook-widget-background-translucent-hover: rgba(20, 20, 20, 0.9);
38+
--gitbook-widget-background-solid: #f0f0f0;
39+
}
40+
}
41+
3242
* {
3343
box-sizing: border-box;
3444
}
@@ -120,7 +130,8 @@
120130
z-index: 9998;
121131
width: calc(min(var(--gitbook-widget-window-width), calc(100vw - var(--gitbook-widget-right) - var(--gitbook-widget-left))));
122132
height: calc(min(var(--gitbook-widget-window-height), calc(100vh - var(--gitbook-widget-window-bottom) - var(--gitbook-widget-top))));
123-
background-color: var(--gitbook-widget-background-solid);
133+
background-color: var(--gitbook-widget-background-translucent);
134+
backdrop-filter: blur(48px);
124135
border: 1px solid var(--gitbook-widget-border-color);
125136
border-radius: var(--gitbook-widget-radius);
126137
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);

packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/(content)/layout.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ export default async function SiteDynamicLayout({
2222
const withTracking = shouldTrackEvents(await headers());
2323

2424
return (
25-
<CustomizationRootLayout forcedTheme={forcedTheme} context={context}>
25+
<CustomizationRootLayout
26+
className="site-background"
27+
forcedTheme={forcedTheme}
28+
context={context}
29+
>
2630
<SiteLayout
2731
context={context}
2832
forcedTheme={forcedTheme}
Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,7 @@
1-
import type { RouteLayoutParams } from '@/app/utils';
21
import { EmbeddableAssistantPage } from '@/components/Embeddable';
3-
import { getEmbeddableDynamicContext } from '@/lib/embeddable';
42

53
export const dynamic = 'force-static';
64

7-
type PageProps = {
8-
params: Promise<RouteLayoutParams>;
9-
};
10-
11-
export default async function Page(props: PageProps) {
12-
const params = await props.params;
13-
const { context } = await getEmbeddableDynamicContext(params);
14-
15-
return <EmbeddableAssistantPage context={context} />;
5+
export default async function Page() {
6+
return <EmbeddableAssistantPage />;
167
}

packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/(content)/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default async function SiteStaticLayout({
1919
const withTracking = shouldTrackEvents();
2020

2121
return (
22-
<CustomizationRootLayout context={context}>
22+
<CustomizationRootLayout className="site-background" context={context}>
2323
<SiteLayout
2424
context={context}
2525
withTracking={withTracking}
Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,7 @@
1-
import type { RouteLayoutParams } from '@/app/utils';
21
import { EmbeddableAssistantPage } from '@/components/Embeddable';
3-
import { getEmbeddableStaticContext } from '@/lib/embeddable';
42

53
export const dynamic = 'force-static';
64

7-
type PageProps = {
8-
params: Promise<RouteLayoutParams>;
9-
};
10-
11-
export default async function Page(props: PageProps) {
12-
const params = await props.params;
13-
const { context } = await getEmbeddableStaticContext(params);
14-
15-
return <EmbeddableAssistantPage context={context} />;
5+
export default async function Page() {
6+
return <EmbeddableAssistantPage />;
167
}

packages/gitbook/src/components/AI/server-actions/AIMessageView.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ export function AIMessageView(
1616
) {
1717
const { message, context, withToolCalls = true, withLinkPreviews = true } = props;
1818

19-
return (
19+
return message.steps.length > 0 ? (
2020
<div className="flex flex-col gap-2">
2121
{message.steps.map((step, index) => {
2222
return (
2323
<div
2424
key={index}
2525
className={tcls(
26-
'flex animate-fade-in-slow flex-col gap-2',
26+
'flex flex-col gap-2',
2727
step.content.nodes.length > 0 ? 'has-content' : ''
2828
)}
2929
>
@@ -35,7 +35,7 @@ export function AIMessageView(
3535
wrapBlocksInSuspense: false,
3636
withLinkPreviews,
3737
}}
38-
style="mt-2 space-y-4 empty:hidden"
38+
style="mt-2 space-y-4 *:origin-top-left *:animate-blur-in-slow"
3939
/>
4040

4141
{withToolCalls && step.toolCalls && step.toolCalls.length > 0 ? (
@@ -45,5 +45,5 @@ export function AIMessageView(
4545
);
4646
})}
4747
</div>
48-
);
48+
) : null;
4949
}

packages/gitbook/src/components/AI/server-actions/AIToolCallsSummary.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ function ToolCallSummary(props: { toolCall: AIToolCall; context: GitBookSiteCont
3939
const { toolCall, context } = props;
4040

4141
return (
42-
<div className="flex origin-left animate-scale-in-slow items-start gap-2 text-sm text-tint-subtle">
42+
<div className="mt-2 flex origin-top-left animate-blur-in-slow items-start gap-2 text-sm text-tint-subtle">
4343
<Icon
4444
icon={getIconForToolCall(toolCall)}
4545
className="mt-1 size-3 shrink-0 text-tint-subtle/8"
@@ -160,7 +160,7 @@ async function DescriptionForSearchToolCall(props: {
160160
const hasResults = toolCall.results.length > 0;
161161

162162
return (
163-
<details className={tcls('-ml-5 group flex w-full flex-col', hasResults ? 'gap-2' : '')}>
163+
<details className="-ml-5 group flex w-full flex-col">
164164
<summary
165165
className={tcls(
166166
'-mx-2 flex list-none items-center gap-2 circular-corners:rounded-2xl rounded-corners:rounded-md pr-4 pl-7 transition-colors marker:hidden',
@@ -187,7 +187,7 @@ async function DescriptionForSearchToolCall(props: {
187187
) : null}
188188
</summary>
189189
{hasResults ? (
190-
<div className="hide-scrollbar mt-1 max-h-0 overflow-y-auto circular-corners:rounded-2xl rounded-corners:rounded-lg border border-tint-subtle p-2 opacity-0 transition-all transition-discrete duration-500 group-open:max-h-96 group-open:opacity-11">
190+
<div className="hide-scrollbar mt-4 max-h-0 overflow-y-auto circular-corners:rounded-2xl rounded-corners:rounded-lg border border-tint-subtle p-2 opacity-0 transition-all transition-discrete duration-500 group-open:max-h-96 group-open:opacity-11">
191191
<ol className="space-y-1">
192192
{searchResultsWithHrefs.map((result, index) => (
193193
<li

packages/gitbook/src/components/AIChat/AIChat.tsx

Lines changed: 74 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
99
import {
1010
type AIChatController,
1111
type AIChatState,
12+
useAI,
1213
useAIChatController,
1314
useAIChatState,
1415
} from '../AI';
@@ -24,15 +25,15 @@ import {
2425
import { useTrackEvent } from '../Insights';
2526
import { useNow } from '../hooks';
2627
import { Button } from '../primitives';
28+
import { ScrollContainer } from '../primitives/ScrollContainer';
2729
import { AIChatControlButton } from './AIChatControlButton';
2830
import { AIChatIcon } from './AIChatIcon';
2931
import { AIChatInput } from './AIChatInput';
3032
import { AIChatMessages } from './AIChatMessages';
3133
import AIChatSuggestedQuestions from './AIChatSuggestedQuestions';
3234

33-
export function AIChat(props: { trademark: boolean }) {
34-
const { trademark } = props;
35-
35+
export function AIChat() {
36+
const { config } = useAI();
3637
const language = useLanguage();
3738
const chat = useAIChatState();
3839
const chatController = useAIChatController();
@@ -70,18 +71,18 @@ export function AIChat(props: { trademark: boolean }) {
7071
<div
7172
data-testid="ai-chat"
7273
className={tcls(
73-
'ai-chat inset-y-0 right-0 z-40 mx-auto flex max-w-3xl scroll-mt-36 px-4 py-4 transition-[width,opacity,margin,display] transition-discrete duration-300 sm:px-6 lg:fixed lg:w-80 depth-flat:lg:p-0 lg:pr-4 lg:pl-0 xl:w-96',
74+
'ai-chat inset-y-0 right-0 z-40 mx-auto flex max-w-3xl scroll-mt-36 px-4 py-4 transition-[width,opacity,margin,display] transition-discrete duration-300 sm:px-6 lg:fixed lg:w-80 lg:p-0 xl:w-96',
7475
chat.opened
7576
? 'lg:starting:ml-0 lg:starting:w-0 lg:starting:opacity-0'
7677
: 'hidden lg:ml-0 lg:w-0! lg:opacity-0'
7778
)}
7879
>
79-
<EmbeddableFrame className="relative shrink-0 circular-corners:rounded-3xl rounded-corners:rounded-md border border-tint-subtle depth-subtle:shadow-lg shadow-tint transition-all duration-300 lg:w-76 depth-flat:lg:rounded-none depth-flat:lg:border-y-0 depth-flat:lg:border-r-0 xl:w-92">
80+
<EmbeddableFrame className="relative shrink-0 border-tint-subtle border-l to-tint-base transition-all duration-300 max-lg:circular-corners:rounded-3xl max-lg:rounded-corners:rounded-md max-lg:border lg:w-80 xl:w-96">
8081
<EmbeddableFrameHeader>
81-
<AIChatDynamicIcon trademark={trademark} />
82+
<AIChatDynamicIcon trademark={config.trademark} />
8283
<EmbeddableFrameHeaderMain>
8384
<EmbeddableFrameTitle>
84-
{getAIChatName(language, trademark)}
85+
{getAIChatName(language, config.trademark)}
8586
</EmbeddableFrameTitle>
8687
<AIChatSubtitle chat={chat} />
8788
</EmbeddableFrameHeaderMain>
@@ -98,7 +99,7 @@ export function AIChat(props: { trademark: boolean }) {
9899
</EmbeddableFrameButtons>
99100
</EmbeddableFrameHeader>
100101
<EmbeddableFrameBody>
101-
<AIChatBody chatController={chatController} chat={chat} trademark={trademark} />
102+
<AIChatBody chatController={chatController} chat={chat} />
102103
</EmbeddableFrameBody>
103104
</EmbeddableFrame>
104105
</div>
@@ -145,10 +146,33 @@ export function AIChatSubtitle(props: {
145146
const language = useLanguage();
146147

147148
return (
148-
<EmbeddableFrameSubtitle className={chat.loading ? 'h-3 opacity-11' : 'h-0 opacity-0'}>
149-
{chat.messages[chat.messages.length - 1]?.content
150-
? tString(language, 'ai_chat_working')
151-
: tString(language, 'ai_chat_thinking')}
149+
<EmbeddableFrameSubtitle
150+
className={tcls('relative', chat.loading ? 'h-3 opacity-11' : 'h-0 opacity-0')}
151+
>
152+
<span
153+
className={tcls(
154+
'absolute left-0',
155+
chat.loading
156+
? chat.messages[chat.messages.length - 1]?.content
157+
? 'animate-blur-in-slow'
158+
: 'hidden'
159+
: 'animate-blur-out-slow'
160+
)}
161+
>
162+
{t(language, 'ai_chat_working')}
163+
</span>
164+
<span
165+
className={tcls(
166+
'absolute left-0',
167+
chat.loading
168+
? chat.messages[chat.messages.length - 1]?.content
169+
? 'animate-blur-out-slow'
170+
: 'animate-blur-in-slow'
171+
: 'hidden'
172+
)}
173+
>
174+
{t(language, 'ai_chat_thinking')}
175+
</span>
152176
</EmbeddableFrameSubtitle>
153177
);
154178
}
@@ -159,20 +183,13 @@ export function AIChatSubtitle(props: {
159183
export function AIChatBody(props: {
160184
chatController: AIChatController;
161185
chat: AIChatState;
162-
trademark: boolean;
163186
welcomeMessage?: string;
164187
suggestions?: string[];
165188
}) {
166-
const { chatController, chat, trademark, suggestions } = props;
189+
const { chatController, chat, suggestions } = props;
190+
const { trademark } = useAI().config;
167191

168192
const [input, setInput] = React.useState('');
169-
170-
const scrollContainerRef = React.useRef<HTMLDivElement>(null);
171-
// Ref for the last user message element
172-
const lastUserMessageRef = React.useRef<HTMLDivElement>(null);
173-
const inputRef = React.useRef<HTMLDivElement>(null);
174-
175-
const [inputHeight, setInputHeight] = React.useState(0);
176193
const language = useLanguage();
177194
const now = useNow(60 * 60 * 1000); // Refresh every hour for greeting
178195

@@ -186,67 +203,42 @@ export function AIChatBody(props: {
186203
return tString(language, 'ai_chat_assistant_greeting_evening');
187204
}, [now, language]);
188205

189-
// Auto-scroll to the latest user message when messages change
190-
React.useEffect(() => {
191-
if (chat.messages.length > 0 && lastUserMessageRef.current) {
192-
lastUserMessageRef.current.scrollIntoView({
193-
behavior: 'smooth',
194-
block: 'start',
195-
});
196-
}
197-
}, [chat.messages.length]);
198-
199-
React.useEffect(() => {
200-
const timeout = setTimeout(() => {
201-
if (lastUserMessageRef.current) {
202-
lastUserMessageRef.current.scrollIntoView({
203-
behavior: 'smooth',
204-
block: 'start',
205-
});
206-
}
207-
}, 100);
208-
209-
// We want the chat messages to scroll underneath the input, but they should scroll past the input when scrolling all the way down.
210-
// The best way to do this is to observe the input height and adjust the padding bottom of the scroll container accordingly.
211-
const observer = new ResizeObserver((entries) => {
212-
entries.forEach((entry) => {
213-
setInputHeight(entry.contentRect.height + 32);
214-
});
215-
});
216-
if (inputRef.current) {
217-
observer.observe(inputRef.current);
218-
}
219-
return () => {
220-
observer.disconnect();
221-
clearTimeout(timeout);
222-
};
223-
}, []);
224-
225206
return (
226207
<>
227-
<div
228-
ref={scrollContainerRef}
229-
className="gutter-stable flex grow scroll-pt-4 flex-col gap-4 overflow-y-auto p-4"
230-
style={{
231-
paddingBottom: `${inputHeight}px`,
232-
}}
208+
<ScrollContainer
209+
className="shrink grow basis-80 animate-fade-in-slow [container-type:size]"
210+
contentClassName="p-4 gutter-stable flex flex-col gap-4"
211+
orientation="vertical"
212+
fadeEdges={['leading']}
213+
active={`message-group-${chat.messages.filter((message) => message.role === 'user').length - 1}`}
233214
>
234215
{isEmpty ? (
235-
<div className="flex min-h-full w-full shrink-0 flex-col items-center justify-center gap-6 py-4">
236-
<div className="flex size-32 animate-fade-in-slow items-center justify-center rounded-full bg-tint-subtle">
237-
<AIChatIcon
238-
state="intro"
239-
trademark={trademark}
240-
className="size-16 animate-[present_500ms_200ms_both]"
241-
/>
242-
</div>
243-
<div className="animate-[fadeIn_500ms_400ms_both]">
244-
<h5 className=" text-center font-bold text-lg text-tint-strong">
245-
{timeGreeting}
246-
</h5>
247-
<p className="text-center text-tint">
248-
{t(language, 'ai_chat_assistant_description')}
249-
</p>
216+
<div className="flex grow flex-col">
217+
<div className="my-auto flex flex-row items-center gap-4 pb-6 [@container(min-height:400px)]:flex-col">
218+
<div
219+
className="flex size-16 shrink-0 animate-scale-in items-center justify-center rounded-full bg-primary-solid/1 [@container(min-height:400px)]:size-32"
220+
style={{ animationDelay: '.3s' }}
221+
>
222+
<AIChatIcon
223+
state="intro"
224+
trademark={trademark}
225+
className="size-8 text-primary [@container(min-height:400px)]:size-16"
226+
/>
227+
</div>
228+
<div className="flex flex-col items-start [@container(min-height:400px)]:items-center">
229+
<h5
230+
className="animate-blur-in-slow font-bold text-lg text-tint-strong [@container(min-height:400px)]:text-center"
231+
style={{ animationDelay: '.5s' }}
232+
>
233+
{timeGreeting}
234+
</h5>
235+
<p
236+
className="animate-blur-in-slow text-tint [@container(min-height:400px)]:text-center"
237+
style={{ animationDelay: '.6s' }}
238+
>
239+
{t(language, 'ai_chat_assistant_description')}
240+
</p>
241+
</div>
250242
</div>
251243
{!chat.error ? (
252244
<AIChatSuggestedQuestions
@@ -256,17 +248,11 @@ export function AIChatBody(props: {
256248
) : null}
257249
</div>
258250
) : (
259-
<AIChatMessages
260-
chat={chat}
261-
chatController={chatController}
262-
lastUserMessageRef={lastUserMessageRef}
263-
/>
251+
<AIChatMessages chat={chat} chatController={chatController} />
264252
)}
265-
</div>
266-
<div
267-
ref={inputRef}
268-
className="absolute inset-x-0 bottom-0 mr-2 flex select-none flex-col gap-4 bg-linear-to-b from-transparent to-50% to-tint-base/9 p-4 pr-2"
269-
>
253+
</ScrollContainer>
254+
255+
<div className="flex flex-col gap-2 px-4 pb-4">
270256
{/* Display an error banner when something went wrong. */}
271257
{chat.error ? <AIChatError chatController={chatController} /> : null}
272258

packages/gitbook/src/components/AIChat/AIChatInput.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function AIChatInput(props: {
5555
);
5656

5757
return (
58-
<div className="depth-subtle:has-[textarea:focus]:-translate-y-px relative flex flex-col overflow-hidden circular-corners:rounded-2xl rounded-corners:rounded-md bg-tint-base/9 depth-subtle:shadow-sm shadow-tint/6 ring-1 ring-tint-subtle backdrop-blur-lg transition-all depth-subtle:has-[textarea:focus]:shadow-lg has-[textarea:focus]:shadow-primary-subtle has-[textarea:focus]:ring-2 has-[textarea:focus]:ring-primary-hover contrast-more:bg-tint-base dark:shadow-tint-1">
58+
<div className="depth-subtle:has-[textarea:focus]:-translate-y-px relative flex animate-blur-in-slow flex-col overflow-hidden circular-corners:rounded-3xl rounded-corners:rounded-xl bg-tint-base/9 depth-subtle:shadow-sm shadow-tint/6 ring-1 ring-tint-subtle backdrop-blur-lg transition-all depth-subtle:has-[textarea:focus]:shadow-lg has-[textarea:focus]:shadow-primary-subtle has-[textarea:focus]:ring-2 has-[textarea:focus]:ring-primary-hover contrast-more:bg-tint-base dark:shadow-tint-1">
5959
<textarea
6060
ref={inputRef}
6161
disabled={disabled || loading}

0 commit comments

Comments
 (0)