Skip to content

Commit bb135e9

Browse files
committed
Add chat component (non-functional)
1 parent 34eed44 commit bb135e9

File tree

7 files changed

+307
-135
lines changed

7 files changed

+307
-135
lines changed

packages/gitbook-v2/src/lib/data/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,5 +189,7 @@ export interface GitBookDataFetcher {
189189
input: api.AIMessageInput[];
190190
output: api.AIOutputFormat;
191191
model: api.AIModel;
192+
tools?: api.AIToolCapabilities;
193+
previousResponseId?: string;
192194
}): AsyncGenerator<api.AIStreamResponse, void, unknown>;
193195
}

packages/gitbook/src/components/Adaptive/server-actions/api.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
'use server';
2-
import { type AIMessageInput, AIModel, type AIStreamResponse } from '@gitbook/api';
2+
import {
3+
type AIMessageInput,
4+
AIModel,
5+
type AIStreamResponse,
6+
type AIToolCapabilities,
7+
} from '@gitbook/api';
38
import type { GitBookBaseContext } from '@v2/lib/context';
49
import { EventIterator } from 'event-iterator';
510
import type { MaybePromise } from 'p-map';
@@ -51,6 +56,7 @@ export async function streamGenerateObject<T>(
5156
schema: z.ZodSchema<T>;
5257
messages: AIMessageInput[];
5358
model?: AIModel;
59+
tools?: AIToolCapabilities;
5460
previousResponseId?: string;
5561
}
5662
) {
Lines changed: 162 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
11
'use client';
2+
import { tcls } from '@/lib/tailwind';
3+
import { filterOutNullable } from '@/lib/typescript';
24
import { Icon } from '@gitbook/icons';
35
import { motion } from 'framer-motion';
46
import { useEffect, useState } from 'react';
57
import { useVisitedPages } from '../Insights/useVisitedPages';
6-
import { streamAISearchSummary } from './server-actions';
8+
import { Button } from '../primitives';
9+
import { isQuestion } from './isQuestion';
10+
import { streamAISearchAnswer, streamAISearchSummary } from './server-actions';
711

8-
export function SearchChat() {
12+
export function SearchChat(props: { query: string }) {
913
// const currentPage = usePageContext();
1014
// const language = useLanguage();
15+
16+
const { query } = props;
17+
1118
const visitedPages = useVisitedPages((state) => state.pages);
1219
const [summary, setSummary] = useState('');
20+
const [messages, setMessages] = useState<
21+
{ role: string; content?: string; fetching?: boolean }[]
22+
>([]);
23+
const [followupQuestions, setFollowupQuestions] = useState<string[]>();
24+
1325
const [responseId, setResponseId] = useState<string | null>(null);
1426

1527
useEffect(() => {
@@ -20,7 +32,6 @@ export function SearchChat() {
2032
visitedPages,
2133
});
2234

23-
let generatedSummary = '';
2435
for await (const data of stream) {
2536
if (cancelled) return;
2637

@@ -29,8 +40,7 @@ export function SearchChat() {
2940
}
3041

3142
if ('summary' in data && data.summary !== undefined) {
32-
generatedSummary = data.summary;
33-
setSummary(generatedSummary);
43+
setSummary(data.summary);
3444
}
3545
}
3646
})();
@@ -40,28 +50,155 @@ export function SearchChat() {
4050
};
4151
}, [visitedPages]);
4252

53+
useEffect(() => {
54+
let cancelled = false;
55+
56+
if (query) {
57+
setMessages([
58+
{
59+
role: 'user',
60+
content: query,
61+
},
62+
{
63+
role: 'assistant',
64+
fetching: true,
65+
},
66+
]);
67+
68+
(async () => {
69+
const stream = await streamAISearchAnswer({
70+
question: query,
71+
previousResponseId: responseId ?? undefined,
72+
});
73+
74+
for await (const data of stream) {
75+
if (cancelled) return;
76+
77+
if ('responseId' in data && data.responseId !== undefined) {
78+
setResponseId(data.responseId);
79+
}
80+
81+
if ('answer' in data && data.answer !== undefined) {
82+
setMessages((prev) => [
83+
...prev.slice(0, -1),
84+
{ role: 'assistant', content: data.answer, fetching: false },
85+
]);
86+
}
87+
88+
if ('followupQuestions' in data && data.followupQuestions !== undefined) {
89+
setFollowupQuestions(data.followupQuestions.filter(filterOutNullable));
90+
}
91+
}
92+
})();
93+
94+
return () => {
95+
cancelled = true;
96+
};
97+
}
98+
}, [query, responseId]);
99+
43100
return (
44-
<motion.div layout="position" className="w-full">
45-
<h5 className="mb-1 flex items-center gap-1 font-semibold text-sm text-tint-subtle">
46-
<Icon icon="glasses-round" className="mt-0.5 size-4" /> Summary of what you've read
47-
</h5>
48-
49-
{summary ? (
50-
summary
51-
) : (
52-
<div key="loading" className="mt-2 flex flex-wrap gap-2">
53-
{[...Array(9)].map((_, index) => (
54-
<div
55-
key={index}
56-
className="h-4 animate-[fadeIn_0.5s_ease-in-out_both,pulse_2s_ease-in-out_infinite] rounded straight-corners:rounded-none bg-tint-active"
57-
style={{
58-
animationDelay: `${index * 0.1}s,${0.5 + index * 0.1}s`,
59-
width: `${((index % 5) + 1) * 15}%`,
60-
}}
61-
/>
62-
))}
101+
<motion.div layout="position" className="relative mx-auto h-full p-8">
102+
<div className="mx-auto flex w-full max-w-prose flex-col gap-4">
103+
<div>
104+
<h5 className="mb-1 flex items-center gap-1 font-semibold text-tint-subtle text-xs">
105+
<Icon icon="glasses-round" className="mt-0.5 size-3" /> Summary of what
106+
you've read
107+
</h5>
108+
109+
{summary ? (
110+
summary
111+
) : (
112+
<div key="loading" className="mt-2 flex flex-wrap gap-2">
113+
{[...Array(9)].map((_, index) => (
114+
<div
115+
key={index}
116+
className="h-4 animate-[fadeIn_0.5s_ease-in-out_both,pulse_2s_ease-in-out_infinite] rounded straight-corners:rounded-none bg-tint-active"
117+
style={{
118+
animationDelay: `${index * 0.1}s,${0.5 + index * 0.1}s`,
119+
width: `${((index % 5) + 1) * 15}%`,
120+
}}
121+
/>
122+
))}
123+
</div>
124+
)}
125+
</div>
126+
127+
{messages.map((message) => (
128+
<div
129+
key={message.content}
130+
className={tcls(
131+
'flex flex-col gap-1',
132+
message.role === 'user' && 'items-end gap-1 self-end'
133+
)}
134+
>
135+
{message.role === 'user' ? (
136+
<h5 className="flex items-center gap-1 font-semibold text-tint-subtle text-xs">
137+
You asked {isQuestion(query) ? '' : 'about'}
138+
</h5>
139+
) : (
140+
<h5 className="flex items-center gap-1 font-semibold text-tint-subtle text-xs">
141+
<Icon icon="sparkle" className="mt-0.5 size-3" /> AI Answer
142+
</h5>
143+
)}
144+
{message.fetching ? (
145+
<div key="loading" className="mt-2 flex flex-wrap gap-2">
146+
{[...Array(9)].map((_, index) => (
147+
<div
148+
key={index}
149+
className="h-4 animate-[fadeIn_0.5s_ease-in-out_both,pulse_2s_ease-in-out_infinite] rounded straight-corners:rounded-none bg-tint-active"
150+
style={{
151+
animationDelay: `${index * 0.1}s,${0.5 + index * 0.1}s`,
152+
width: `${((index % 5) + 1) * 15}%`,
153+
}}
154+
/>
155+
))}
156+
</div>
157+
) : (
158+
<div
159+
className={tcls(
160+
message.role === 'user' && 'rounded-lg bg-tint-active px-4 py-2'
161+
)}
162+
>
163+
{message.content}
164+
</div>
165+
)}
166+
</div>
167+
))}
168+
</div>
169+
170+
{query ? (
171+
<div className="absolute inset-x-0 bottom-0 border-tint-subtle border-t bg-tint-subtle px-8 py-4">
172+
<div className="mx-auto flex w-full max-w-prose flex-col gap-2">
173+
{followupQuestions && followupQuestions.length > 0 && (
174+
<div className="flex gap-2 overflow-x-auto">
175+
{followupQuestions?.map((question) => (
176+
<div
177+
className="whitespace-nowrap rounded straight-corners:rounded-sm border border-tint-subtle bg-tint-base px-2 py-1 text-sm"
178+
key={question}
179+
>
180+
{question}
181+
</div>
182+
))}
183+
</div>
184+
)}
185+
<div className="flex gap-2">
186+
<input
187+
type="text"
188+
placeholder="Ask a follow-up question"
189+
className="grow rounded px-4 py-1 ring-1 ring-tint-subtle"
190+
/>
191+
<Button
192+
label="Send"
193+
iconOnly
194+
icon="arrow-up"
195+
size="medium"
196+
className="shrink-0"
197+
/>
198+
</div>
199+
</div>
63200
</div>
64-
)}
201+
) : null}
65202
</motion.div>
66203
);
67204
}

packages/gitbook/src/components/Search/SearchModal.tsx

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import { useHotkeys } from 'react-hotkeys-hook';
66

77
import { tString, useLanguage } from '@/intl/client';
88
import { tcls } from '@/lib/tailwind';
9-
10-
import { Button } from '../primitives/Button';
119
import { LoadingPane } from '../primitives/LoadingPane';
1210
import { SearchAskProvider, useSearchAskState } from './SearchAskContext';
1311
import { SearchChat } from './SearchChat';
@@ -220,8 +218,8 @@ function SearchModalBody(
220218
'bg-tint-base',
221219
'max-w-screen-lg',
222220
'mx-auto',
223-
'min-h-[30dvh]',
224-
'max-h-[70dvh]',
221+
// 'min-h-[50dvh]',
222+
'h-[70dvh]',
225223
'w-full',
226224
'rounded-lg',
227225
'straight-corners:rounded-sm',
@@ -317,28 +315,14 @@ function SearchModalBody(
317315
key="chat"
318316
layout
319317
className={tcls(
320-
'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',
318+
'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',
321319
state.mode === 'chat' && 'md:col-start-1'
322320
)}
323321
initial={{ width: 0 }}
324322
animate={{ width: '100%' }}
325323
exit={{ width: 0 }}
326324
>
327-
{state.mode === 'chat' ? (
328-
<Button
329-
icon="right-from-line"
330-
iconOnly
331-
label="Show results"
332-
variant="blank"
333-
className="px-2"
334-
onClick={() => {
335-
setSearchState((prev) =>
336-
prev ? { ...prev, mode: 'both', manual: true } : null
337-
);
338-
}}
339-
/>
340-
) : null}
341-
<SearchChat />
325+
<SearchChat query={normalizedQuery} />
342326
</motion.div>
343327
) : null}
344328
</AnimatePresence>

packages/gitbook/src/components/Search/SearchQuestionResultItem.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ export const SearchQuestionResultItem = React.forwardRef(function SearchQuestion
4242
]
4343
)}
4444
{...getLinkProp({
45-
ask: true,
4645
query: question,
4746
})}
4847
>

0 commit comments

Comments
 (0)