mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-10-09 23:05:20 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d846548c8 | |||
| 8e4c94a4e5 |
@@ -23,6 +23,8 @@ export type AIChatMessage = {
|
|||||||
query?: string;
|
query?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AIChatSize = 'default' | 'large';
|
||||||
|
|
||||||
export type AIChatPendingTool = {
|
export type AIChatPendingTool = {
|
||||||
icon?: IconName;
|
icon?: IconName;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -85,6 +87,11 @@ export type AIChatState = {
|
|||||||
* display an error alert. Clearing the conversation will reset this flag.
|
* display an error alert. Clearing the conversation will reset this flag.
|
||||||
*/
|
*/
|
||||||
error: boolean;
|
error: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The size of the chat window.
|
||||||
|
*/
|
||||||
|
size: AIChatSize;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AIChatController = {
|
export type AIChatController = {
|
||||||
@@ -96,12 +103,14 @@ export type AIChatController = {
|
|||||||
postMessage: (input: { message: string }) => void;
|
postMessage: (input: { message: string }) => void;
|
||||||
/** Clear the conversation */
|
/** Clear the conversation */
|
||||||
clear: () => void;
|
clear: () => void;
|
||||||
|
/** Toggle the size of the chat window */
|
||||||
|
setSize: (size: AIChatSize) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AIChatControllerContext = React.createContext<AIChatController | null>(null);
|
const AIChatControllerContext = React.createContext<AIChatController | null>(null);
|
||||||
|
|
||||||
// Global state store for AI chat
|
// Global state store for AI chat
|
||||||
const globalState = zustand.create<AIChatState>(() => {
|
export const globalState = zustand.create<AIChatState>(() => {
|
||||||
return {
|
return {
|
||||||
opened: false,
|
opened: false,
|
||||||
responseId: null,
|
responseId: null,
|
||||||
@@ -112,6 +121,7 @@ const globalState = zustand.create<AIChatState>(() => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
error: false,
|
error: false,
|
||||||
initialQuery: null,
|
initialQuery: null,
|
||||||
|
size: 'default',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -438,14 +448,19 @@ export function AIChatProvider(props: {
|
|||||||
}));
|
}));
|
||||||
}, [setSearchState]);
|
}, [setSearchState]);
|
||||||
|
|
||||||
|
const onSetSize = React.useCallback((size: AIChatSize) => {
|
||||||
|
globalState.setState((state) => ({ ...state, size }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const controller = React.useMemo(() => {
|
const controller = React.useMemo(() => {
|
||||||
return {
|
return {
|
||||||
open: onOpen,
|
open: onOpen,
|
||||||
close: onClose,
|
close: onClose,
|
||||||
clear: onClear,
|
clear: onClear,
|
||||||
postMessage: onPostMessage,
|
postMessage: onPostMessage,
|
||||||
|
setSize: onSetSize,
|
||||||
};
|
};
|
||||||
}, [onOpen, onClose, onClear, onPostMessage]);
|
}, [onOpen, onClose, onClear, onPostMessage, onSetSize]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AIChatControllerContext.Provider value={controller}>
|
<AIChatControllerContext.Provider value={controller}>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { t, tString, useLanguage } from '@/intl/client';
|
import { t, tString, useLanguage } from '@/intl/client';
|
||||||
import type { TranslationLanguage } from '@/intl/translations';
|
import type { TranslationLanguage } from '@/intl/translations';
|
||||||
|
import { tcls } from '@/lib/tailwind';
|
||||||
import { Icon } from '@gitbook/icons';
|
import { Icon } from '@gitbook/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
@@ -13,7 +14,7 @@ import {
|
|||||||
} from '../AI';
|
} from '../AI';
|
||||||
import { EmbeddableFrame } from '../Embeddable/EmbeddableFrame';
|
import { EmbeddableFrame } from '../Embeddable/EmbeddableFrame';
|
||||||
import { useNow } from '../hooks';
|
import { useNow } from '../hooks';
|
||||||
import { Button } from '../primitives';
|
import { Button, DropdownMenuSeparator } from '../primitives';
|
||||||
import { DropdownMenu, DropdownMenuItem } from '../primitives';
|
import { DropdownMenu, DropdownMenuItem } from '../primitives';
|
||||||
import { AIChatIcon } from './AIChatIcon';
|
import { AIChatIcon } from './AIChatIcon';
|
||||||
import { AIChatInput } from './AIChatInput';
|
import { AIChatInput } from './AIChatInput';
|
||||||
@@ -64,7 +65,10 @@ export function AIChat(props: { trademark: boolean }) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid="ai-chat"
|
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
|
<EmbeddableFrame
|
||||||
className="relative circular-corners:rounded-3xl rounded-corners:rounded-md depth-subtle:shadow-lg shadow-tint ring-1 ring-tint-subtle"
|
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
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
chatController.clear();
|
chatController.clear();
|
||||||
@@ -223,27 +254,32 @@ export function AIChatBody(props: {
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
ref={scrollContainerRef}
|
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={{
|
style={{
|
||||||
paddingBottom: `${inputHeight}px`,
|
paddingBottom: `${inputHeight}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isEmpty ? (
|
{isEmpty ? (
|
||||||
<div className="flex min-h-full w-full shrink-0 flex-col items-center justify-center gap-6 py-4">
|
<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">
|
<div className="flex items-center gap-4 lg:flex-col">
|
||||||
<AIChatIcon
|
<div className="flex animate-fade-in-slow items-center justify-center rounded-full bg-tint-subtle p-4 lg:p-8">
|
||||||
state="intro"
|
<AIChatIcon
|
||||||
trademark={trademark}
|
state="intro"
|
||||||
className="size-16 animate-[present_500ms_200ms_both]"
|
trademark={trademark}
|
||||||
/>
|
className="size-8 animate-[present_500ms_200ms_both] lg:size-16"
|
||||||
</div>
|
/>
|
||||||
<div className="animate-[fadeIn_500ms_400ms_both]">
|
</div>
|
||||||
<h5 className=" text-center font-bold text-lg text-tint-strong">
|
<div className="animate-[fadeIn_500ms_400ms_both]">
|
||||||
{timeGreeting}
|
<h5 className="font-bold text-lg text-tint-strong lg:text-center">
|
||||||
</h5>
|
{timeGreeting}
|
||||||
<p className="text-center text-tint">
|
</h5>
|
||||||
{t(language, 'ai_chat_assistant_description')}
|
<p className="text-tint lg:text-center">
|
||||||
</p>
|
{t(language, 'ai_chat_assistant_description')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!chat.error ? (
|
{!chat.error ? (
|
||||||
<AIChatSuggestedQuestions chatController={chatController} />
|
<AIChatSuggestedQuestions chatController={chatController} />
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export function AIChatInput(props: {
|
|||||||
<span className="leading-none">
|
<span className="leading-none">
|
||||||
{t(language, 'ai_chat_context_title')}
|
{t(language, 'ai_chat_context_title')}
|
||||||
</span>
|
</span>
|
||||||
<Icon icon="question-circle" className="size-3" />
|
<Icon icon="question-circle" className="size-3 shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
</HoverCardRoot>
|
</HoverCardRoot>
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ export default function AIChatSuggestedQuestions(props: { chatController: AIChat
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
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) => (
|
{DEFAULT_SUGGESTED_QUESTIONS.map((question, index) => (
|
||||||
<Button
|
<Button
|
||||||
key={question}
|
key={question}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="medium"
|
size="medium"
|
||||||
className="max-w-full animate-[present_500ms_both] whitespace-normal"
|
className="shrink grow animate-[present_500ms_both] whitespace-normal"
|
||||||
style={{
|
style={{
|
||||||
animationDelay: `${800 + index * 100}ms`,
|
animationDelay: `${800 + index * 100}ms`,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import type {
|
|||||||
GitBookIntegrationEventCallback,
|
GitBookIntegrationEventCallback,
|
||||||
GitBookIntegrationTool,
|
GitBookIntegrationTool,
|
||||||
} from '@gitbook/browser-types';
|
} from '@gitbook/browser-types';
|
||||||
import type { Assistant } from '../AI';
|
import { type Assistant, globalState } from '../AI';
|
||||||
|
|
||||||
const events = new Map<GitBookIntegrationEvent, GitBookIntegrationEventCallback[]>();
|
const events = new Map<GitBookIntegrationEvent, GitBookIntegrationEventCallback[]>();
|
||||||
|
|
||||||
@@ -67,6 +67,24 @@ if (typeof window !== 'undefined') {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
window.GitBook = gitbookGlobal;
|
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',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user