{
+ chatController.open();
+ }, [chatController]);
+
// Track the view of the AI chat
const trackEvent = useTrackEvent();
React.useEffect(() => {
@@ -48,26 +57,30 @@ export function EmbeddableAIChat() {
return (
-
-
-
-
- {getAIChatName(language, config.trademark)}
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ {getAIChatName(language, config.trademark)}
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/packages/gitbook/src/components/Embeddable/EmbeddableDocsPage.tsx b/packages/gitbook/src/components/Embeddable/EmbeddableDocsPage.tsx
index ee8b26a536..87de3e7d4a 100644
--- a/packages/gitbook/src/components/Embeddable/EmbeddableDocsPage.tsx
+++ b/packages/gitbook/src/components/Embeddable/EmbeddableDocsPage.tsx
@@ -1,18 +1,24 @@
import { type PagePathParams, getSitePageData } from '@/components/SitePage';
-
-import { PageBody } from '@/components/PageBody';
import type { GitBookSiteContext } from '@/lib/context';
import { SiteInsightsDisplayContext } from '@gitbook/api';
import type { Metadata } from 'next';
-import { Button } from '../primitives';
+import { HeaderMobileMenu } from '../Header/HeaderMobileMenu';
+import { PageBody } from '../PageBody';
+import { SiteSectionTabs, encodeClientSiteSections } from '../SiteSections';
+import { TableOfContents } from '../TableOfContents';
+import { ScrollContainer } from '../primitives/ScrollContainer';
+import { EmbeddableDocsPageControlButtons } from './EmbeddableDocsPageControlButtons';
import {
EmbeddableFrame,
EmbeddableFrameBody,
EmbeddableFrameButtons,
EmbeddableFrameHeader,
EmbeddableFrameHeaderMain,
+ EmbeddableFrameMain,
+ EmbeddableFrameSidebar,
+ EmbeddableFrameTitle,
} from './EmbeddableFrame';
-import { EmbeddableIframeButtons } from './EmbeddableIframeAPI';
+import { EmbeddableIframeButtons, EmbeddableIframeTabs } from './EmbeddableIframeAPI';
export const dynamic = 'force-static';
@@ -33,32 +39,54 @@ export async function EmbeddableDocsPage(props: EmbeddableDocsPageProps) {
return (
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ {context.site.title}
+
+
+
+
+
+ {context.sections ? (
+
+ ) : null}
-
+
+
+
+
+
+
+
);
}
diff --git a/packages/gitbook/src/components/Embeddable/EmbeddableDocsPageControlButtons.tsx b/packages/gitbook/src/components/Embeddable/EmbeddableDocsPageControlButtons.tsx
new file mode 100644
index 0000000000..722b5018bd
--- /dev/null
+++ b/packages/gitbook/src/components/Embeddable/EmbeddableDocsPageControlButtons.tsx
@@ -0,0 +1,22 @@
+'use client';
+
+import { tString, useLanguage } from '@/intl/client';
+import { Button } from '../primitives';
+
+export function EmbeddableDocsPageControlButtons(props: { href: string }) {
+ const { href } = props;
+ const language = useLanguage();
+
+ return (
+
+ );
+}
diff --git a/packages/gitbook/src/components/Embeddable/EmbeddableFrame.tsx b/packages/gitbook/src/components/Embeddable/EmbeddableFrame.tsx
index f46fa37e96..3dd1d63a73 100644
--- a/packages/gitbook/src/components/Embeddable/EmbeddableFrame.tsx
+++ b/packages/gitbook/src/components/Embeddable/EmbeddableFrame.tsx
@@ -17,7 +17,7 @@ export const EmbeddableFrame = React.forwardRef
) {
+ const { children, ...rest } = props;
+
+ return (
+
+ {children}
+
+ );
+}
+
export function EmbeddableFrameHeader(props: {
children: React.ReactNode;
}) {
const { children } = props;
return (
-
+
{children}
);
@@ -45,7 +55,7 @@ export function EmbeddableFrameHeaderMain(props: {
}) {
const { children } = props;
- return
{children}
;
+ return
{children}
;
}
export function EmbeddableFrameBody(props: {
@@ -82,10 +92,21 @@ export function EmbeddableFrameSubtitle(props: {
);
}
+export function EmbeddableFrameSidebar(props: { children: React.ReactNode }) {
+ const { children } = props;
+
+ return (
+
+ {children}
+
+ );
+}
+
export function EmbeddableFrameButtons(props: {
+ className?: string;
children: React.ReactNode;
}) {
- const { children } = props;
+ const { children, className } = props;
- return
{children}
;
+ return
{children}
;
}
diff --git a/packages/gitbook/src/components/Embeddable/EmbeddableIframeAPI.tsx b/packages/gitbook/src/components/Embeddable/EmbeddableIframeAPI.tsx
index f37dd4c55f..1d9fbb495b 100644
--- a/packages/gitbook/src/components/Embeddable/EmbeddableIframeAPI.tsx
+++ b/packages/gitbook/src/components/Embeddable/EmbeddableIframeAPI.tsx
@@ -4,17 +4,23 @@ import type { GitBookEmbeddableConfiguration, ParentToFrameMessage } from '@gitb
import { createChannel } from 'bidc';
import React from 'react';
-import { useAIChatController } from '@/components/AI';
+import { useAI, useAIChatController } from '@/components/AI';
+import { CustomizationAIMode } from '@gitbook/api';
import { useRouter } from 'next/navigation';
import { createStore, useStore } from 'zustand';
import { integrationsAssistantTools } from '../Integrations';
import { Button } from '../primitives';
-const embeddableConfiguration = createStore
(() => ({
- buttons: [],
- welcomeMessage: '',
+const embeddableConfiguration = createStore<
+ GitBookEmbeddableConfiguration & { baseURL: string; siteTitle: string }
+>(() => ({
+ tabs: [],
+ actions: [],
+ greeting: { title: '', subtitle: '' },
suggestions: [],
tools: [],
+ baseURL: '',
+ siteTitle: '',
}));
/**
@@ -22,12 +28,24 @@ const embeddableConfiguration = createStore(() =
*/
export function EmbeddableIframeAPI(props: {
baseURL: string;
+ siteTitle: string;
}) {
- const { baseURL } = props;
+ const { baseURL, siteTitle } = props;
const router = useRouter();
const chatController = useAIChatController();
+ React.useEffect(() => {
+ embeddableConfiguration.setState({ baseURL });
+ embeddableConfiguration.setState({ siteTitle });
+ }, [baseURL, siteTitle]);
+
+ React.useEffect(() => {
+ return chatController.on('postMessage', () => {
+ router.push(`${baseURL}/assistant`);
+ });
+ }, [router, baseURL, chatController]);
+
React.useEffect(() => {
if (window.parent === window) {
return;
@@ -50,6 +68,7 @@ export function EmbeddableIframeAPI(props: {
chatController.postMessage({
message: message.message,
});
+ router.push(`${baseURL}/assistant`);
break;
}
case 'configure': {
@@ -82,7 +101,9 @@ export function EmbeddableIframeAPI(props: {
/**
* Hook to get the configuration from the parent window.
*/
-export function useEmbeddableConfiguration(
+export function useEmbeddableConfiguration<
+ T = GitBookEmbeddableConfiguration & { baseURL: string; siteTitle: string },
+>(
// @ts-expect-error - This is a workaround to allow the function to be optional.
fn: (state: GitBookEmbeddableConfiguration) => T = (state) => state
) {
@@ -93,23 +114,109 @@ export function useEmbeddableConfiguration(
* Display the buttons defined by the parent window.
*/
export function EmbeddableIframeButtons() {
- const buttons = useEmbeddableConfiguration((state) => state.buttons);
+ const actions = useEmbeddableConfiguration((state) => state.actions);
return (
<>
- {buttons.map((button) => (
+ {actions.length > 0 && (
+
+ )}
+ {actions.map((action, index) => (
+ active={isOpen}
+ {...props}
+ />
);
}
diff --git a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx
index d2ea495a5f..e176a3f826 100644
--- a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx
+++ b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx
@@ -33,6 +33,7 @@ export function SiteSectionTabs(props: {
children,
} = props;
+ const containerRef = React.useRef(null);
const currentTriggerRef = React.useRef(null);
const [offset, setOffset] = React.useState(null);
const [value, setValue] = React.useState();
@@ -41,12 +42,15 @@ export function SiteSectionTabs(props: {
React.useEffect(() => {
const trigger = currentTriggerRef.current;
- if (!value || !trigger) {
+ const container = containerRef.current;
+ if (!value || !trigger || !container) {
return;
}
- const triggerWidth = trigger.getBoundingClientRect().width;
- const triggerLeft = trigger.getBoundingClientRect().left;
+ const triggerWidth = trigger.getBoundingClientRect().width - SCREEN_OFFSET;
+ const triggerLeft =
+ trigger.getBoundingClientRect().left -
+ (window.innerWidth - container.getBoundingClientRect().width) / 2;
setOffset(triggerLeft + triggerWidth / 2);
}, [value]);
@@ -58,6 +62,7 @@ export function SiteSectionTabs(props: {
'page-default-width:2xl:px-[calc((100%-1536px+4rem)/2)]',
className
)}
+ ref={containerRef}
value={value}
onValueChange={setValue}
skipDelayDuration={500}
@@ -141,14 +146,14 @@ export function SiteSectionTabs(props: {
{children}
{header && header}
diff --git a/packages/gitbook/src/components/TableOfContents/Trademark.tsx b/packages/gitbook/src/components/TableOfContents/Trademark.tsx
index bd077d84cf..f5dbe8387d 100644
--- a/packages/gitbook/src/components/TableOfContents/Trademark.tsx
+++ b/packages/gitbook/src/components/TableOfContents/Trademark.tsx
@@ -13,7 +13,9 @@ import { Link } from '../primitives';
export function Trademark(props: {
context: GitBookSpaceContext;
placement: SiteInsightsTrademarkPlacement;
+ className?: string;
}) {
+ const { className, ...rest } = props;
return (
-
+
);
}
@@ -69,8 +73,9 @@ export function Trademark(props: {
export function TrademarkLink(props: {
context: GitBookSpaceContext;
placement: SiteInsightsTrademarkPlacement;
+ className?: string;
}) {
- const { context, placement } = props;
+ const { context, placement, className } = props;
const { space } = context;
const language = getSpaceLanguage(context);
@@ -85,9 +90,6 @@ export function TrademarkLink(props: {
href={url.toString()}
className={tcls(
'text-sm',
- // 'lg:max-xl:page-no-toc:text-xs',
- // 'lg:max-xl:page-no-toc:px-3',
- // 'lg:max-xl:page-no-toc:py-3',
'font-semibold',
'text-tint',
@@ -113,29 +115,17 @@ export function TrademarkLink(props: {
'ring-tint-subtle',
'transition-colors',
- 'pointer-events-auto'
+ 'pointer-events-auto',
+
+ className
)}
insights={{
type: 'trademark_click',
placement,
}}
>
-
-
- {t(language, 'powered_by_gitbook')}
-
+
+ {t(language, 'powered_by_gitbook')}
);
}
diff --git a/packages/gitbook/src/components/primitives/Button.tsx b/packages/gitbook/src/components/primitives/Button.tsx
index 4d649d3bed..79bb7e48a1 100644
--- a/packages/gitbook/src/components/primitives/Button.tsx
+++ b/packages/gitbook/src/components/primitives/Button.tsx
@@ -7,7 +7,7 @@ import { type ClassValue, tcls } from '@/lib/tailwind';
import { Icon, type IconName } from '@gitbook/icons';
import { Link, type LinkInsightsProps } from './Link';
import { useClassnames } from './StyleProvider';
-import { Tooltip } from './Tooltip';
+import { Tooltip, type TooltipProps } from './Tooltip';
export type ButtonProps = {
href?: string;
@@ -20,6 +20,7 @@ export type ButtonProps = {
trailing?: React.ReactNode;
children?: React.ReactNode;
active?: boolean;
+ tooltipProps?: TooltipProps;
} & LinkInsightsProps &
React.HTMLAttributes;
@@ -112,6 +113,7 @@ export const Button = React.forwardRef<
active,
trailing,
disabled,
+ tooltipProps,
...rest
},
ref
@@ -133,18 +135,29 @@ export const Button = React.forwardRef<
);
const buttonOnlyClassNames = useClassnames(['ButtonStyles']);
+ let iconElement = null;
+ if (icon) {
+ if (React.isValidElement(icon)) {
+ type IconElement = React.ReactElement>;
+ iconElement = React.cloneElement(icon as IconElement, {
+ className: tcls(
+ 'button-leading-icon size-[1em] shrink-0',
+ (icon as IconElement).props.className
+ ),
+ });
+ } else {
+ iconElement = (
+
+ );
+ }
+ }
+
const content = (
<>
- {icon ? (
- typeof icon === 'string' ? (
-
- ) : (
- icon
- )
- ) : null}
+ {iconElement}
{iconOnly || (!children && !label) ? null : (
{children ?? label}
)}
@@ -184,9 +197,13 @@ export const Button = React.forwardRef<
return (children || iconOnly) && label ? (
{button}
diff --git a/packages/gitbook/src/components/primitives/Link.tsx b/packages/gitbook/src/components/primitives/Link.tsx
index 80ae283a2d..13bf3bd68f 100644
--- a/packages/gitbook/src/components/primitives/Link.tsx
+++ b/packages/gitbook/src/components/primitives/Link.tsx
@@ -78,8 +78,8 @@ export function Link(props: LinkProps) {
const onClick = (event: React.MouseEvent) => {
const isExternalWithOrigin = isExternalLink(href, window.location.origin);
- // Only trigger navigation context for internal links without modifier keys (i.e. open in new tab).
- if (!isExternal && !event.ctrlKey && !event.metaKey) {
+ // Only trigger navigation context for internal links in the same window without modifier keys (i.e. open in new tab).
+ if (!isExternal && target !== '_blank' && !event.ctrlKey && !event.metaKey) {
onNavigationClick(href);
}
diff --git a/packages/gitbook/src/components/primitives/Tooltip.tsx b/packages/gitbook/src/components/primitives/Tooltip.tsx
index 7e2a710df2..1f9f389b46 100644
--- a/packages/gitbook/src/components/primitives/Tooltip.tsx
+++ b/packages/gitbook/src/components/primitives/Tooltip.tsx
@@ -3,12 +3,18 @@
import { tcls } from '@/lib/tailwind';
import * as RadixTooltip from '@radix-ui/react-tooltip';
+export type TooltipProps = {
+ rootProps?: RadixTooltip.TooltipProps;
+ triggerProps?: RadixTooltip.TooltipTriggerProps;
+ contentProps?: RadixTooltip.TooltipContentProps;
+};
+
export function Tooltip(props: {
children: React.ReactNode;
label?: string | React.ReactNode;
- triggerProps?: RadixTooltip.TooltipTriggerProps;
- contentProps?: RadixTooltip.TooltipContentProps;
- rootProps?: RadixTooltip.TooltipProps;
+ triggerProps?: TooltipProps['triggerProps'];
+ contentProps?: TooltipProps['contentProps'];
+ rootProps?: TooltipProps['rootProps'];
arrow?: boolean;
className?: string;
}) {
@@ -30,6 +36,7 @@ export function Tooltip(props: {