Compare commits

...

2 Commits

Author SHA1 Message Date
Zeno Kapitein 5d846548c8 Update AIChat.tsx 2025-08-22 19:08:59 +02:00
Zeno Kapitein 8e4c94a4e5 First test 2025-08-22 19:00:09 +02:00
5 changed files with 92 additions and 23 deletions
@@ -23,6 +23,8 @@ export type AIChatMessage = {
query?: string;
};
export type AIChatSize = 'default' | 'large';
export type AIChatPendingTool = {
icon?: IconName;
label: string;
@@ -85,6 +87,11 @@ export type AIChatState = {
* display an error alert. Clearing the conversation will reset this flag.
*/
error: boolean;
/**
* The size of the chat window.
*/
size: AIChatSize;
};
export type AIChatController = {
@@ -96,12 +103,14 @@ export type AIChatController = {
postMessage: (input: { message: string }) => void;
/** Clear the conversation */
clear: () => void;
/** Toggle the size of the chat window */
setSize: (size: AIChatSize) => void;
};
const AIChatControllerContext = React.createContext<AIChatController | null>(null);
// Global state store for AI chat
const globalState = zustand.create<AIChatState>(() => {
export const globalState = zustand.create<AIChatState>(() => {
return {
opened: false,
responseId: null,
@@ -112,6 +121,7 @@ const globalState = zustand.create<AIChatState>(() => {
loading: false,
error: false,
initialQuery: null,
size: 'default',
};
});
@@ -438,14 +448,19 @@ export function AIChatProvider(props: {
}));
}, [setSearchState]);
const onSetSize = React.useCallback((size: AIChatSize) => {
globalState.setState((state) => ({ ...state, size }));
}, []);
const controller = React.useMemo(() => {
return {
open: onOpen,
close: onClose,
clear: onClear,
postMessage: onPostMessage,
setSize: onSetSize,
};
}, [onOpen, onClose, onClear, onPostMessage]);
}, [onOpen, onClose, onClear, onPostMessage, onSetSize]);
return (
<AIChatControllerContext.Provider value={controller}>
@@ -2,6 +2,7 @@
import { t, tString, useLanguage } from '@/intl/client';
import type { TranslationLanguage } from '@/intl/translations';
import { tcls } from '@/lib/tailwind';
import { Icon } from '@gitbook/icons';
import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
@@ -13,7 +14,7 @@ import {
} from '../AI';
import { EmbeddableFrame } from '../Embeddable/EmbeddableFrame';
import { useNow } from '../hooks';
import { Button } from '../primitives';
import { Button, DropdownMenuSeparator } from '../primitives';
import { DropdownMenu, DropdownMenuItem } from '../primitives';
import { AIChatIcon } from './AIChatIcon';
import { AIChatInput } from './AIChatInput';
@@ -64,7 +65,10 @@ export function AIChat(props: { trademark: boolean }) {
return (
<div
data-testid="ai-chat"
className="ai-chat inset-y-0 right-0 z-40 mx-auto flex max-w-3xl animate-present scroll-mt-36 px-4 py-4 transition-all duration-300 sm:px-6 lg:fixed lg:w-80 lg:animate-enter-from-right lg:pr-4 lg:pl-0 xl:w-96"
className={tcls(
'ai-chat z-40 mx-auto flex max-h-full w-full max-w-3xl animate-present scroll-mt-36 p-4 transition-all duration-300 max-md:inset-x-0 max-md:bottom-0 lg:fixed lg:inset-y-0 lg:right-0 lg:animate-enter-from-right',
chat.size === 'large' ? 'lg:w-104 xl:w-156' : 'lg:w-80 xl:w-96'
)}
>
<EmbeddableFrame
className="relative circular-corners:rounded-3xl rounded-corners:rounded-md depth-subtle:shadow-lg shadow-tint ring-1 ring-tint-subtle"
@@ -92,6 +96,33 @@ export function AIChat(props: { trademark: boolean }) {
/>
}
>
<DropdownMenuItem
onClick={() => {
chatController.setSize(
chat.size === 'default' ? 'large' : 'default'
);
}}
>
<Icon
icon={
chat.size === 'default'
? 'arrow-up-right-and-arrow-down-left-from-center'
: 'arrow-down-left-and-arrow-up-right-to-center'
}
className="size-3 shrink-0 text-tint-subtle"
/>
<div className="flex flex-col gap-0.5">
<p className="font-medium">
{chat.size === 'default' ? 'Maximize' : 'Minimize'}
</p>
<p className="text-tint text-xs">
{chat.size === 'default'
? 'Longer, more detailed answers'
: 'Shorter, more concise answers'}
</p>
</div>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => {
chatController.clear();
@@ -223,27 +254,32 @@ export function AIChatBody(props: {
<>
<div
ref={scrollContainerRef}
className="flex grow scroll-pt-4 flex-col gap-4 overflow-y-auto p-4"
className={tcls(
'flex grow scroll-pt-4 flex-col gap-4 overflow-y-auto p-4 transition-all',
chat.size === 'large' ? 'text-base' : 'text-sm'
)}
style={{
paddingBottom: `${inputHeight}px`,
}}
>
{isEmpty ? (
<div className="flex min-h-full w-full shrink-0 flex-col items-center justify-center gap-6 py-4">
<div className="flex size-32 animate-fade-in-slow items-center justify-center rounded-full bg-tint-subtle">
<AIChatIcon
state="intro"
trademark={trademark}
className="size-16 animate-[present_500ms_200ms_both]"
/>
</div>
<div className="animate-[fadeIn_500ms_400ms_both]">
<h5 className=" text-center font-bold text-lg text-tint-strong">
{timeGreeting}
</h5>
<p className="text-center text-tint">
{t(language, 'ai_chat_assistant_description')}
</p>
<div className="flex items-center gap-4 lg:flex-col">
<div className="flex animate-fade-in-slow items-center justify-center rounded-full bg-tint-subtle p-4 lg:p-8">
<AIChatIcon
state="intro"
trademark={trademark}
className="size-8 animate-[present_500ms_200ms_both] lg:size-16"
/>
</div>
<div className="animate-[fadeIn_500ms_400ms_both]">
<h5 className="font-bold text-lg text-tint-strong lg:text-center">
{timeGreeting}
</h5>
<p className="text-tint lg:text-center">
{t(language, 'ai_chat_assistant_description')}
</p>
</div>
</div>
{!chat.error ? (
<AIChatSuggestedQuestions chatController={chatController} />
@@ -140,7 +140,7 @@ export function AIChatInput(props: {
<span className="leading-none">
{t(language, 'ai_chat_context_title')}
</span>
<Icon icon="question-circle" className="size-3" />
<Icon icon="question-circle" className="size-3 shrink-0" />
</div>
</HoverCardTrigger>
</HoverCardRoot>
@@ -13,13 +13,13 @@ export default function AIChatSuggestedQuestions(props: { chatController: AIChat
];
return (
<div className="flex flex-col items-center gap-2">
<div className="flex items-center gap-2 lg:flex-col">
{DEFAULT_SUGGESTED_QUESTIONS.map((question, index) => (
<Button
key={question}
variant="secondary"
size="medium"
className="max-w-full animate-[present_500ms_both] whitespace-normal"
className="shrink grow animate-[present_500ms_both] whitespace-normal"
style={{
animationDelay: `${800 + index * 100}ms`,
}}
@@ -9,7 +9,7 @@ import type {
GitBookIntegrationEventCallback,
GitBookIntegrationTool,
} from '@gitbook/browser-types';
import type { Assistant } from '../AI';
import { type Assistant, globalState } from '../AI';
const events = new Map<GitBookIntegrationEvent, GitBookIntegrationEventCallback[]>();
@@ -67,6 +67,24 @@ if (typeof window !== 'undefined') {
},
};
window.GitBook = gitbookGlobal;
window.GitBook?.registerTool({
name: 'ExpandChatSize',
description:
'FREE TO USE: Expand the chat to a larger size. Use this tool before you start writing your final answer if you need to show a lot of content, like step-by-step instructions or codeblocks. You do not need to preface this tool with feedback to the user, just call it and the chat will expand.',
execute: async () => {
globalState.setState((state) => ({ ...state, size: 'large' }));
return {
output: {
expanded: true,
},
summary: {
icon: 'arrow-up-right-and-arrow-down-left-from-center',
text: 'Chat size expanded',
},
};
},
});
}
/**