1
1
'use client' ;
2
+ import { tcls } from '@/lib/tailwind' ;
3
+ import { filterOutNullable } from '@/lib/typescript' ;
2
4
import { Icon } from '@gitbook/icons' ;
3
5
import { motion } from 'framer-motion' ;
4
6
import { useEffect , useState } from 'react' ;
5
7
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' ;
7
11
8
- export function SearchChat ( ) {
12
+ export function SearchChat ( props : { query : string } ) {
9
13
// const currentPage = usePageContext();
10
14
// const language = useLanguage();
15
+
16
+ const { query } = props ;
17
+
11
18
const visitedPages = useVisitedPages ( ( state ) => state . pages ) ;
12
19
const [ summary , setSummary ] = useState ( '' ) ;
20
+ const [ messages , setMessages ] = useState <
21
+ { role : string ; content ?: string ; fetching ?: boolean } [ ]
22
+ > ( [ ] ) ;
23
+ const [ followupQuestions , setFollowupQuestions ] = useState < string [ ] > ( ) ;
24
+
13
25
const [ responseId , setResponseId ] = useState < string | null > ( null ) ;
14
26
15
27
useEffect ( ( ) => {
@@ -20,7 +32,6 @@ export function SearchChat() {
20
32
visitedPages,
21
33
} ) ;
22
34
23
- let generatedSummary = '' ;
24
35
for await ( const data of stream ) {
25
36
if ( cancelled ) return ;
26
37
@@ -29,8 +40,7 @@ export function SearchChat() {
29
40
}
30
41
31
42
if ( 'summary' in data && data . summary !== undefined ) {
32
- generatedSummary = data . summary ;
33
- setSummary ( generatedSummary ) ;
43
+ setSummary ( data . summary ) ;
34
44
}
35
45
}
36
46
} ) ( ) ;
@@ -40,28 +50,155 @@ export function SearchChat() {
40
50
} ;
41
51
} , [ visitedPages ] ) ;
42
52
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
+
43
100
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 >
63
200
</ div >
64
- ) }
201
+ ) : null }
65
202
</ motion . div >
66
203
) ;
67
204
}
0 commit comments