Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/gitbook/src/components/AIChat/AIChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ import { ScrollContainer } from '../primitives/ScrollContainer';
import { SideSheet } from '../primitives/SideSheet';
import { AIChatControl } from './AIChatControl';
import { AIChatControlButton } from './AIChatControlButton';
import { AIChatExpandButton } from './AIChatExpandButton';
import { AIChatIcon } from './AIChatIcon';
import { AIChatInput } from './AIChatInput';
import { AIChatMessages } from './AIChatMessages';
import { AIChatResizeHandle } from './AIChatResizeHandle';
import AIChatSuggestedQuestions from './AIChatSuggestedQuestions';

export function AIChat() {
Expand Down Expand Up @@ -85,9 +87,10 @@ export function AIChat() {
withOverlay={true}
data-ai-chat
className={tcls(
'ai-chat mx-auto ml-8 not-hydrated:hidden w-96 transition-[width] duration-300 ease-quint lg:max-xl:w-80'
'ai-chat mx-auto ml-8 not-hydrated:hidden w-96 transition-[width] duration-300 ease-quint lg:w-(--ai-chat-width)'
)}
>
<AIChatResizeHandle />
<EmbeddableFrame className="relative w-full shrink-0 border-tint-subtle border-l to-tint-base">
<EmbeddableFrameMain data-testid="ai-chat" aria-busy={chat.loading}>
<EmbeddableFrameHeader className="not-embed:px-4">
Expand All @@ -100,6 +103,7 @@ export function AIChat() {
</EmbeddableFrameHeaderMain>
<EmbeddableFrameButtons>
<AIChatControlButton />
<AIChatExpandButton />
<Button
onClick={() => chatController.close()}
iconOnly
Expand Down
35 changes: 35 additions & 0 deletions packages/gitbook/src/components/AIChat/AIChatExpandButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use client';

import { tString, useLanguage } from '@/intl/client';
import { Icon } from '@gitbook/icons';
import { Button } from '../primitives';
import { useAIChatWidthStore, useIsAIChatMaxWidth } from './useAIChatWidthStore';

export function AIChatExpandButton() {
const language = useLanguage();
const toggleWidth = useAIChatWidthStore((state) => state.toggleWidth);
const isMaxWidth = useIsAIChatMaxWidth();

return (
<Button
onClick={toggleWidth}
iconOnly
icon={
<Icon
icon={
isMaxWidth
? 'arrow-down-left-and-arrow-up-right-to-center'
: 'arrow-up-right-and-arrow-down-left-from-center'
}
className="scale-90"
/>
}
label={tString(
language,
isMaxWidth ? 'ai_chat_collapse_panel' : 'ai_chat_expand_panel'
)}
variant="blank"
className="max-lg:hidden"
/>
);
}
83 changes: 83 additions & 0 deletions packages/gitbook/src/components/AIChat/AIChatResizeHandle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use client';

import { tcls } from '@/lib/tailwind';
import React from 'react';
import { useAIChatWidthStore } from './useAIChatWidthStore';

function setResizing(active: boolean) {
document.documentElement.dataset.aiChatResizing = String(active);
}

export function AIChatResizeHandle() {
const setWidth = useAIChatWidthStore((state) => state.setWidth);
const frameRef = React.useRef<number | null>(null);
const widthRef = React.useRef(0);

React.useEffect(() => {
const onResize = () => useAIChatWidthStore.getState().syncWidth();
window.addEventListener('resize', onResize);
return () => {
window.removeEventListener('resize', onResize);
if (frameRef.current !== null) {
cancelAnimationFrame(frameRef.current);
}
setResizing(false);
};
}, []);

const stopResizing = (event: React.PointerEvent<HTMLDivElement>) => {
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
event.currentTarget.releasePointerCapture(event.pointerId);
}
if (frameRef.current !== null) {
cancelAnimationFrame(frameRef.current);
frameRef.current = null;
}
setResizing(false);
};

const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
event.preventDefault();
event.currentTarget.setPointerCapture(event.pointerId);
widthRef.current = useAIChatWidthStore.getState().width;
setResizing(true);
};

const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
if (!event.currentTarget.hasPointerCapture(event.pointerId)) {
return;
}
// Panel is right-anchored, so its width is the distance from the cursor to the right edge.
widthRef.current = window.innerWidth - event.clientX;
if (frameRef.current === null) {
frameRef.current = requestAnimationFrame(() => {
frameRef.current = null;
widthRef.current = setWidth(widthRef.current);
});
}
};

const handlePointerUp = (event: React.PointerEvent<HTMLDivElement>) => {
if (!event.currentTarget.hasPointerCapture(event.pointerId)) {
return;
}
setWidth(widthRef.current);
stopResizing(event);
};

return (
<div
aria-hidden="true"
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerCancel={stopResizing}
className={tcls(
'group -translate-x-1/2 absolute inset-y-0 left-0 z-10 hidden w-3 cursor-col-resize touch-none lg:flex',
'items-stretch justify-center'
)}
>
<span className="h-full w-px rounded-full bg-transparent transition-all duration-150 ease-out group-hover:w-0.5 group-hover:bg-primary-solid/40 group-active:w-0.5 group-active:bg-primary-solid" />
</div>
);
}
78 changes: 78 additions & 0 deletions packages/gitbook/src/components/AIChat/useAIChatWidthStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'use client';

import {
getLocalStorageItem,
removeLocalStorageItem,
setLocalStorageItem,
} from '@/lib/browser/local-storage';
import { create } from 'zustand';
import { type StorageValue, persist } from 'zustand/middleware';

const AI_CHAT_MIN_WIDTH = 384;
const AI_CHAT_MAX_WIDTH = 640;
const MIN_CONTENT_WIDTH = 720;

type AIChatWidthStore = {
width: number;
toggleWidth: () => void;
setWidth: (width: number) => number;
syncWidth: () => void;
};

type PersistedState = Pick<AIChatWidthStore, 'width'>;

export const useAIChatWidthStore = create<AIChatWidthStore>()(
persist(
(set, get) => ({
width: AI_CHAT_MIN_WIDTH,
toggleWidth: () =>
get().setWidth(
get().width >= AI_CHAT_MAX_WIDTH ? AI_CHAT_MIN_WIDTH : AI_CHAT_MAX_WIDTH
),
setWidth: (width) => {
const clamped = clampWidth(width);
if (get().width !== clamped) {
set({ width: clamped });
}
setWidthOnViewport(clamped);
return clamped;
},
syncWidth: () => setWidthOnViewport(get().width),
}),
{
name: '@gitbook/ai-chat-width',
storage: {
getItem: (name) =>
getLocalStorageItem<StorageValue<PersistedState> | null>(name, null),
setItem: (name, value) => setLocalStorageItem(name, value),
removeItem: (name) => removeLocalStorageItem(name),
},
partialize: (state) => ({ width: state.width }),
onRehydrateStorage: () => (state) => state?.syncWidth(),
}
)
);

/**
* Whether the panel is at its maximum width.
*/
export const useIsAIChatMaxWidth = () =>
useAIChatWidthStore((state) => state.width >= AI_CHAT_MAX_WIDTH);

// Hoisted so the synchronous persist rehydrate (during create() above) can call them before this point.
function setWidthOnViewport(width: number) {
if (typeof document !== 'undefined') {
document.documentElement.style.setProperty('--ai-chat-width', `${capToViewport(width)}px`);
}
}

function clampWidth(width: number) {
return Math.min(AI_CHAT_MAX_WIDTH, Math.max(AI_CHAT_MIN_WIDTH, Math.round(width)));
}

// Cap a width so the remaining content keeps a usable minimum at the current viewport.
function capToViewport(width: number) {
return typeof window === 'undefined'
? width
: Math.min(width, Math.max(AI_CHAT_MIN_WIDTH, window.innerWidth - MIN_CONTENT_WIDTH));
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function AnnouncementBanner(props: {
className="theme-bold:bg-header-background pt-4 pb-2"
data-nosnippet=""
>
<div className="transition-all duration-300 motion-reduce:transition-none lg:chat-open:pr-80 xl:chat-open:pr-96">
<div className="transition-all duration-300 motion-reduce:transition-none lg:chat-open:pr-(--ai-chat-width)">
<div className={tcls('relative', CONTAINER_STYLE)}>
<Tag
href={contentRef?.href ?? ''}
Expand Down
3 changes: 1 addition & 2 deletions packages/gitbook/src/components/Cookies/CookiesToast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
'max-w-md',
'text-balance',
'sm:left-auto',
'lg:chat-open:mr-80',
'xl:chat-open:mr-100',
'lg:chat-open:mr-(--ai-chat-width)',
'transition-all',
'motion-reduce:transition-none',
'duration-300',
Expand Down
2 changes: 1 addition & 1 deletion packages/gitbook/src/components/Footer/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function Footer(props: { context: GitBookSiteContext }) {
mobileOnly ? 'xl:hidden' : null
)}
>
<div className="transition-[padding] duration-300 motion-reduce:transition-none lg:chat-open:pr-80 xl:chat-open:pr-96">
<div className="transition-[padding] duration-300 motion-reduce:transition-none lg:chat-open:pr-(--ai-chat-width)">
<div
className={tcls(
CONTAINER_STYLE,
Expand Down
4 changes: 2 additions & 2 deletions packages/gitbook/src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export async function Header(props: {
'site-header:theme-bold:shadow-tint-12/2'
)}
>
<div className="transition-all duration-300 motion-reduce:transition-none lg:chat-open:pr-80 xl:chat-open:pr-96">
<div className="transition-all duration-300 motion-reduce:transition-none lg:chat-open:pr-(--ai-chat-width)">
<div
data-gb-header-content
className={tcls(
Expand Down Expand Up @@ -206,7 +206,7 @@ export async function Header(props: {
</div>

{visibleSections && withSections ? (
<div className="transition-[padding] duration-300 motion-reduce:transition-none lg:chat-open:pr-80 xl:chat-open:pr-96">
<div className="transition-[padding] duration-300 motion-reduce:transition-none lg:chat-open:pr-(--ai-chat-width)">
<SiteSectionTabs sections={encodeClientSiteSections(context, visibleSections)}>
{variants.translations.length > 1 ? (
<TranslationsDropdown
Expand Down
10 changes: 10 additions & 0 deletions packages/gitbook/src/components/RootLayout/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@
@apply leading-relaxed;
interpolate-size: allow-keywords; /* Opt-in for modern browsers to interpolate "auto" values in transitions/animations. */
overflow-x: hidden; /* We never want horizontal scroll of the whole page, it looks buggy and we should never have overflow anyway */
--ai-chat-width: 24rem; /* Default AI chat panel width (= AI_CHAT_DEFAULT_WIDTH 384px); overridden client-side from local storage. */
}

/* Modern browsers with `scrollbar-*` support */
Expand Down Expand Up @@ -305,6 +306,15 @@ html.dark {
color-scheme: dark light;
}

/** While the AI chat panel is being resized, suppress transitions */
html[data-ai-chat-resizing="true"] {
cursor: col-resize;
user-select: none;
}
html[data-ai-chat-resizing="true"] * {
transition-duration: 0s !important;
}

html.announcement-hidden [data-gb-announcement-banner] {
@apply hidden;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export function SpaceLayout(props: SpaceLayoutProps) {
) : null}

{/* Chat panel shifts content left when open */}
<div className="motion-safe:transition-all motion-safe:duration-300 lg:chat-open:mr-80 xl:chat-open:mr-96">
<div className="motion-safe:transition-all motion-safe:duration-300 lg:chat-open:mr-(--ai-chat-width)">
<div
className={tcls(
'flex',
Expand Down
2 changes: 2 additions & 0 deletions packages/gitbook/src/intl/translations/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ export const ar: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'مساء الخير',
ai_chat_assistant_greeting_night: 'تصبح على خير',
ai_chat_clear_conversation: 'مسح المحادثة',
ai_chat_expand_panel: 'توسيع اللوحة',
ai_chat_collapse_panel: 'طي اللوحة',
ai_chat_thinking: 'جار التفكير...',
ai_chat_working: 'جار العمل...',
ai_chat_exploring: 'جار الاستكشاف...',
Expand Down
2 changes: 2 additions & 0 deletions packages/gitbook/src/intl/translations/bg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ export const bg: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Добър вечер',
ai_chat_assistant_greeting_night: 'Лека нощ',
ai_chat_clear_conversation: 'Изчистване на разговора',
ai_chat_expand_panel: 'Разгъване на панела',
ai_chat_collapse_panel: 'Свиване на панела',
ai_chat_thinking: 'Мисля...',
ai_chat_working: 'Работя...',
ai_chat_exploring: 'Проучвам...',
Expand Down
2 changes: 2 additions & 0 deletions packages/gitbook/src/intl/translations/cs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ export const cs: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Dobrý večer',
ai_chat_assistant_greeting_night: 'Dobrou noc',
ai_chat_clear_conversation: 'Vymazat konverzaci',
ai_chat_expand_panel: 'Rozbalit panel',
ai_chat_collapse_panel: 'Sbalit panel',
ai_chat_thinking: 'Přemýšlím...',
ai_chat_working: 'Pracuji...',
ai_chat_exploring: 'Prozkoumávám...',
Expand Down
2 changes: 2 additions & 0 deletions packages/gitbook/src/intl/translations/da.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ export const da: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Godaften',
ai_chat_assistant_greeting_night: 'Godnat',
ai_chat_clear_conversation: 'Ryd samtale',
ai_chat_expand_panel: 'Udvid panel',
ai_chat_collapse_panel: 'Formindsk panel',
ai_chat_thinking: 'Tænker...',
ai_chat_working: 'Arbejder...',
ai_chat_exploring: 'Udforsker...',
Expand Down
2 changes: 2 additions & 0 deletions packages/gitbook/src/intl/translations/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ export const de = {
ai_chat_assistant_greeting_evening: 'Guten Abend',
ai_chat_assistant_greeting_night: 'Gute Nacht',
ai_chat_clear_conversation: 'Unterhaltung löschen',
ai_chat_expand_panel: 'Panel erweitern',
ai_chat_collapse_panel: 'Panel reduzieren',
ai_chat_thinking: 'Denke nach...',
ai_chat_working: 'Arbeite...',
ai_chat_exploring: 'Erkunde...',
Expand Down
2 changes: 2 additions & 0 deletions packages/gitbook/src/intl/translations/el.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ export const el: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Καλό βράδυ',
ai_chat_assistant_greeting_night: 'Καληνύχτα',
ai_chat_clear_conversation: 'Εκκαθάριση συνομιλίας',
ai_chat_expand_panel: 'Ανάπτυξη πίνακα',
ai_chat_collapse_panel: 'Σύμπτυξη πίνακα',
ai_chat_thinking: 'Σκέφτομαι...',
ai_chat_working: 'Εργάζομαι...',
ai_chat_exploring: 'Εξερευνώ...',
Expand Down
2 changes: 2 additions & 0 deletions packages/gitbook/src/intl/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ export const en = {
ai_chat_assistant_greeting_evening: 'Good evening',
ai_chat_assistant_greeting_night: 'Good night',
ai_chat_clear_conversation: 'Clear conversation',
ai_chat_expand_panel: 'Expand panel',
ai_chat_collapse_panel: 'Collapse panel',
ai_chat_thinking: 'Thinking…',
ai_chat_working: 'Working…',
ai_chat_exploring: 'Exploring…',
Expand Down
2 changes: 2 additions & 0 deletions packages/gitbook/src/intl/translations/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ export const es: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Buenas tardes',
ai_chat_assistant_greeting_night: 'Buenas noches',
ai_chat_clear_conversation: 'Limpiar conversación',
ai_chat_expand_panel: 'Expandir panel',
ai_chat_collapse_panel: 'Contraer panel',
ai_chat_thinking: 'Pensando...',
ai_chat_working: 'Trabajando...',
ai_chat_exploring: 'Explorando...',
Expand Down
2 changes: 2 additions & 0 deletions packages/gitbook/src/intl/translations/et.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ export const et: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Tere õhtust',
ai_chat_assistant_greeting_night: 'Head ööd',
ai_chat_clear_conversation: 'Tühjenda vestlus',
ai_chat_expand_panel: 'Laienda paneeli',
ai_chat_collapse_panel: 'Ahenda paneeli',
ai_chat_thinking: 'Mõtlen...',
ai_chat_working: 'Töötan...',
ai_chat_exploring: 'Uurin...',
Expand Down
2 changes: 2 additions & 0 deletions packages/gitbook/src/intl/translations/fi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ export const fi: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Hyvää iltaa',
ai_chat_assistant_greeting_night: 'Hyvää yötä',
ai_chat_clear_conversation: 'Tyhjennä keskustelu',
ai_chat_expand_panel: 'Laajenna paneeli',
ai_chat_collapse_panel: 'Pienennä paneeli',
ai_chat_thinking: 'Ajatellaan...',
ai_chat_working: 'Työskennellään...',
ai_chat_exploring: 'Tutkitaan...',
Expand Down
Loading
Loading