From 5a410feda08e6c8d5874df47f33cc76d5d7d2fd4 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Tue, 13 May 2025 10:50:10 +0200 Subject: [PATCH] Add chat component (non-functional) --- packages/gitbook-v2/src/lib/data/types.ts | 2 + .../components/Adaptive/server-actions/api.ts | 8 +- .../src/components/Search/SearchChat.tsx | 187 +++++++++++++++--- .../src/components/Search/SearchModal.tsx | 24 +-- .../Search/SearchQuestionResultItem.tsx | 1 - .../src/components/Search/SearchResults.tsx | 179 +++++++++-------- .../src/components/Search/server-actions.tsx | 41 +++- 7 files changed, 307 insertions(+), 135 deletions(-) diff --git a/packages/gitbook-v2/src/lib/data/types.ts b/packages/gitbook-v2/src/lib/data/types.ts index 178a0ba77..0b6920523 100644 --- a/packages/gitbook-v2/src/lib/data/types.ts +++ b/packages/gitbook-v2/src/lib/data/types.ts @@ -189,5 +189,7 @@ export interface GitBookDataFetcher { input: api.AIMessageInput[]; output: api.AIOutputFormat; model: api.AIModel; + tools?: api.AIToolCapabilities; + previousResponseId?: string; }): AsyncGenerator; } diff --git a/packages/gitbook/src/components/Adaptive/server-actions/api.ts b/packages/gitbook/src/components/Adaptive/server-actions/api.ts index a1396987d..1ea43e8a5 100644 --- a/packages/gitbook/src/components/Adaptive/server-actions/api.ts +++ b/packages/gitbook/src/components/Adaptive/server-actions/api.ts @@ -1,5 +1,10 @@ 'use server'; -import { type AIMessageInput, AIModel, type AIStreamResponse } from '@gitbook/api'; +import { + type AIMessageInput, + AIModel, + type AIStreamResponse, + type AIToolCapabilities, +} from '@gitbook/api'; import type { GitBookBaseContext } from '@v2/lib/context'; import { EventIterator } from 'event-iterator'; import type { MaybePromise } from 'p-map'; @@ -51,6 +56,7 @@ export async function streamGenerateObject( schema: z.ZodSchema; messages: AIMessageInput[]; model?: AIModel; + tools?: AIToolCapabilities; previousResponseId?: string; } ) { diff --git a/packages/gitbook/src/components/Search/SearchChat.tsx b/packages/gitbook/src/components/Search/SearchChat.tsx index 778b43e87..87b528b94 100644 --- a/packages/gitbook/src/components/Search/SearchChat.tsx +++ b/packages/gitbook/src/components/Search/SearchChat.tsx @@ -1,15 +1,27 @@ 'use client'; +import { tcls } from '@/lib/tailwind'; +import { filterOutNullable } from '@/lib/typescript'; import { Icon } from '@gitbook/icons'; import { motion } from 'framer-motion'; import { useEffect, useState } from 'react'; import { useVisitedPages } from '../Insights/useVisitedPages'; -import { streamAISearchSummary } from './server-actions'; +import { Button } from '../primitives'; +import { isQuestion } from './isQuestion'; +import { streamAISearchAnswer, streamAISearchSummary } from './server-actions'; -export function SearchChat() { +export function SearchChat(props: { query: string }) { // const currentPage = usePageContext(); // const language = useLanguage(); + + const { query } = props; + const visitedPages = useVisitedPages((state) => state.pages); const [summary, setSummary] = useState(''); + const [messages, setMessages] = useState< + { role: string; content?: string; fetching?: boolean }[] + >([]); + const [followupQuestions, setFollowupQuestions] = useState(); + const [responseId, setResponseId] = useState(null); useEffect(() => { @@ -20,7 +32,6 @@ export function SearchChat() { visitedPages, }); - let generatedSummary = ''; for await (const data of stream) { if (cancelled) return; @@ -29,8 +40,7 @@ export function SearchChat() { } if ('summary' in data && data.summary !== undefined) { - generatedSummary = data.summary; - setSummary(generatedSummary); + setSummary(data.summary); } } })(); @@ -40,28 +50,155 @@ export function SearchChat() { }; }, [visitedPages]); - return ( - -
- Summary of what you've read -
+ useEffect(() => { + let cancelled = false; - {summary ? ( - summary - ) : ( -
- {[...Array(9)].map((_, index) => ( -
- ))} + if (query) { + setMessages([ + { + role: 'user', + content: query, + }, + { + role: 'assistant', + fetching: true, + }, + ]); + + (async () => { + const stream = await streamAISearchAnswer({ + question: query, + previousResponseId: responseId ?? undefined, + }); + + for await (const data of stream) { + if (cancelled) return; + + if ('responseId' in data && data.responseId !== undefined) { + setResponseId(data.responseId); + } + + if ('answer' in data && data.answer !== undefined) { + setMessages((prev) => [ + ...prev.slice(0, -1), + { role: 'assistant', content: data.answer, fetching: false }, + ]); + } + + if ('followupQuestions' in data && data.followupQuestions !== undefined) { + setFollowupQuestions(data.followupQuestions.filter(filterOutNullable)); + } + } + })(); + + return () => { + cancelled = true; + }; + } + }, [query, responseId]); + + return ( + +
+
+
+ Summary of what + you've read +
+ + {summary ? ( + summary + ) : ( +
+ {[...Array(9)].map((_, index) => ( +
+ ))} +
+ )}
- )} + + {messages.map((message) => ( +
+ {message.role === 'user' ? ( +
+ You asked {isQuestion(query) ? '' : 'about'} +
+ ) : ( +
+ AI Answer +
+ )} + {message.fetching ? ( +
+ {[...Array(9)].map((_, index) => ( +
+ ))} +
+ ) : ( +
+ {message.content} +
+ )} +
+ ))} +
+ + {query ? ( +
+
+ {followupQuestions && followupQuestions.length > 0 && ( +
+ {followupQuestions?.map((question) => ( +
+ {question} +
+ ))} +
+ )} +
+ +
+
+
+ ) : null} ); } diff --git a/packages/gitbook/src/components/Search/SearchModal.tsx b/packages/gitbook/src/components/Search/SearchModal.tsx index b8ed1daec..8d04feada 100644 --- a/packages/gitbook/src/components/Search/SearchModal.tsx +++ b/packages/gitbook/src/components/Search/SearchModal.tsx @@ -6,8 +6,6 @@ import { useHotkeys } from 'react-hotkeys-hook'; import { tString, useLanguage } from '@/intl/client'; import { tcls } from '@/lib/tailwind'; - -import { Button } from '../primitives/Button'; import { LoadingPane } from '../primitives/LoadingPane'; import { SearchAskProvider, useSearchAskState } from './SearchAskContext'; import { SearchChat } from './SearchChat'; @@ -220,8 +218,8 @@ function SearchModalBody( 'bg-tint-base', 'max-w-screen-lg', 'mx-auto', - 'min-h-[30dvh]', - 'max-h-[70dvh]', + // 'min-h-[50dvh]', + 'h-[70dvh]', 'w-full', 'rounded-lg', 'straight-corners:rounded-sm', @@ -317,28 +315,14 @@ function SearchModalBody( key="chat" layout className={tcls( - 'md:-col-end-1 flex items-start gap-4 overflow-y-auto overflow-x-hidden border-tint-subtle bg-tint-subtle p-8 max-md:border-t md:row-start-2 md:border-l', + 'md:-col-end-1 overflow-y-auto overflow-x-hidden border-tint-subtle bg-tint-subtle max-md:border-t md:row-start-2 md:border-l', state.mode === 'chat' && 'md:col-start-1' )} initial={{ width: 0 }} animate={{ width: '100%' }} exit={{ width: 0 }} > - {state.mode === 'chat' ? ( -