11'use client' ;
2+ import { tcls } from '@/lib/tailwind' ;
3+ import { filterOutNullable } from '@/lib/typescript' ;
24import { Icon } from '@gitbook/icons' ;
35import { motion } from 'framer-motion' ;
46import { useEffect , useState } from 'react' ;
57import { 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}
0 commit comments