@@ -6,6 +6,7 @@ import Image from "next/image";
66import { useCallback , useEffect , useRef , useState } from "react" ;
77import { toast } from "sonner" ;
88import { motion , AnimatePresence } from "framer-motion" ;
9+ import markdownit from "markdown-it" ;
910import { v4 as uuid } from "uuid" ;
1011import { sendGAEvent } from "@next/third-parties/google" ;
1112
@@ -28,6 +29,7 @@ const DEFAULT_MESSAGES = [
2829 { id : 2 , text : "Show me examples of past projects" } ,
2930 { id : 3 , text : "Schedule a free consultation" }
3031] ;
32+ const ENABLE_STREAMING = true ;
3133
3234// Functions
3335// APIs
@@ -86,6 +88,74 @@ const fetchAsk = async (payload: {
8688 return data ;
8789} ;
8890
91+ const fetchAskStream = async (
92+ payload : {
93+ chatInput : string ;
94+ sessionId : string ;
95+ } ,
96+ onChunk : ( chunk : string ) => void
97+ ) : Promise < void > => {
98+ const { chatInput, sessionId } = payload ;
99+ const url = new URL (
100+ String ( process . env . NEXT_PUBLIC_LANDING_POST_CHATS_WEBHOOK )
101+ ) ;
102+ url . searchParams . set ( "stream" , "true" ) ;
103+
104+ const response = await fetch ( url , {
105+ method : "POST" ,
106+ headers : {
107+ "Content-Type" : "application/json"
108+ } ,
109+ body : JSON . stringify ( {
110+ chatInput,
111+ sessionId
112+ } )
113+ } ) ;
114+
115+ if ( ! response . ok ) {
116+ throw new Error ( "Failed to ask question" ) ;
117+ }
118+
119+ const reader = response . body ?. getReader ( ) ;
120+ const decoder = new TextDecoder ( ) ;
121+
122+ if ( ! reader ) {
123+ throw new Error ( "No response body" ) ;
124+ }
125+
126+ let buffer = "" ;
127+
128+ while ( true ) {
129+ const { done, value } = await reader . read ( ) ;
130+ if ( done ) break ;
131+
132+ buffer += decoder . decode ( value , { stream : true } ) ;
133+ const lines = buffer . split ( "\n" ) ;
134+
135+ // Keep the last incomplete line in the buffer
136+ buffer = lines . pop ( ) || "" ;
137+
138+ for ( const line of lines ) {
139+ if ( line . startsWith ( "data: " ) ) {
140+ try {
141+ const jsonStr = line . slice ( 6 ) ; // Remove "data: " prefix
142+ const data = JSON . parse ( jsonStr ) as { chunk : string ; done : boolean } ;
143+
144+ if ( data . chunk ) {
145+ onChunk ( data . chunk ) ;
146+ }
147+
148+ if ( data . done ) {
149+ return ;
150+ }
151+ } catch ( error ) {
152+ console . error ( "Failed to parse SSE data:" , error ) ;
153+ }
154+ }
155+ }
156+ }
157+ } ;
158+
89159// Cookies
90160function setCookie ( cname : string , cvalue : string , exdays : number ) {
91161 const d = new Date ( ) ;
@@ -210,7 +280,7 @@ export default function LandingAIAgent({ gaEvent }: HyperBotToggleProps) {
210280
211281 // Function to handle form submission
212282 const handleSubmit = useCallback (
213- async ( text : string ) => {
283+ async ( text : string , useStreaming : boolean = true ) => {
214284 if ( ! text . length ) return ;
215285 if ( ! text . length || ! inputRef . current ) return ;
216286
@@ -226,16 +296,36 @@ export default function LandingAIAgent({ gaEvent }: HyperBotToggleProps) {
226296 const prevMessages = messages ;
227297 const newMessages = [ ...prevMessages , { human : chatInput , ai : "" } ] ;
228298 setMessages ( newMessages ) ;
229- const { output } = await fetchAsk ( {
230- chatInput,
231- sessionId : sessionId as string
232- } ) ;
233299
234- const newMessagesFromAI = [
235- ...prevMessages ,
236- { human : chatInput , ai : output }
237- ] ;
238- setMessages ( newMessagesFromAI ) ;
300+ if ( useStreaming ) {
301+ let accumulatedOutput = "" ;
302+
303+ await fetchAskStream (
304+ {
305+ chatInput,
306+ sessionId : sessionId as string
307+ } ,
308+ ( chunk ) => {
309+ accumulatedOutput += chunk ;
310+ const updatedMessages = [
311+ ...prevMessages ,
312+ { human : chatInput , ai : accumulatedOutput }
313+ ] ;
314+ setMessages ( updatedMessages ) ;
315+ }
316+ ) ;
317+ } else {
318+ const { output } = await fetchAsk ( {
319+ chatInput,
320+ sessionId : sessionId as string
321+ } ) ;
322+
323+ const newMessagesFromAI = [
324+ ...prevMessages ,
325+ { human : chatInput , ai : output }
326+ ] ;
327+ setMessages ( newMessagesFromAI ) ;
328+ }
239329
240330 scrollToBottom ( ) ;
241331 } catch ( error ) {
@@ -257,7 +347,7 @@ export default function LandingAIAgent({ gaEvent }: HyperBotToggleProps) {
257347
258348 // Wait for the chat to open and then submit
259349 setTimeout ( ( ) => {
260- handleSubmit ( message ) ;
350+ handleSubmit ( message , ENABLE_STREAMING ) ;
261351 } , 300 ) ;
262352 } ;
263353
@@ -303,16 +393,17 @@ export default function LandingAIAgent({ gaEvent }: HyperBotToggleProps) {
303393 ) }
304394 { m . ai && (
305395 < div className = "max-w-[80%] self-start rounded-xl bg-white p-3" >
306- { isSubmitting && i === messages . length - 1 ? (
307- < div className = "flex gap-1" >
308- < span className = "sr-only" > Loading...</ span >
309- < div className = "h-2 w-2 animate-bounce rounded-full bg-gray-500 [animation-delay:-0.3s]" />
310- < div className = "h-2 w-2 animate-bounce rounded-full bg-gray-500 [animation-delay:-0.15s]" />
311- < div className = "h-2 w-2 animate-bounce rounded-full bg-gray-500" />
312- </ div >
313- ) : (
314- < MarkdownContent input = { m . ai } />
315- ) }
396+ < MarkdownContent input = { m . ai } />
397+ </ div >
398+ ) }
399+ { ! m . ai && isSubmitting && i === messages . length - 1 && (
400+ < div className = "max-w-[80%] self-start rounded-xl bg-white p-3" >
401+ < div className = "flex gap-1" >
402+ < span className = "sr-only" > Loading...</ span >
403+ < div className = "h-2 w-2 animate-bounce rounded-full bg-gray-500 [animation-delay:-0.3s]" />
404+ < div className = "h-2 w-2 animate-bounce rounded-full bg-gray-500 [animation-delay:-0.15s]" />
405+ < div className = "h-2 w-2 animate-bounce rounded-full bg-gray-500" />
406+ </ div >
316407 </ div >
317408 ) }
318409 </ div >
@@ -339,15 +430,6 @@ export default function LandingAIAgent({ gaEvent }: HyperBotToggleProps) {
339430 </ div >
340431 ) }
341432
342- { isSubmitting && (
343- < div className = "mt-3 mb-5 flex items-center gap-2 bg-transparent text-xs text-gray-500" >
344- < span className = "sr-only" > Loading...</ span >
345- < div className = "h-2 w-2 animate-bounce rounded-full bg-gray-500 [animation-delay:-0.3s]" />
346- < div className = "h-2 w-2 animate-bounce rounded-full bg-gray-500 [animation-delay:-0.15s]" />
347- < div className = "h-2 w-2 animate-bounce rounded-full bg-gray-500" />
348- </ div >
349- ) }
350-
351433 < div className = "relative flex w-full items-center" >
352434 < input
353435 ref = { inputRef }
@@ -359,7 +441,7 @@ export default function LandingAIAgent({ gaEvent }: HyperBotToggleProps) {
359441 onKeyDown = { ( e ) => {
360442 if ( e . key === "Enter" && ! e . shiftKey ) {
361443 e . preventDefault ( ) ;
362- handleSubmit ( text ) ;
444+ handleSubmit ( text , ENABLE_STREAMING ) ;
363445 }
364446 } }
365447 placeholder = "Ask me about services, success stories, or your challenges"
@@ -370,7 +452,7 @@ export default function LandingAIAgent({ gaEvent }: HyperBotToggleProps) {
370452 variant = "default"
371453 disabled = { isSubmitting }
372454 onClick = { ( ) => {
373- handleSubmit ( text ) ;
455+ handleSubmit ( text , ENABLE_STREAMING ) ;
374456 } } >
375457 < Image
376458 alt = "Send message to AI"
@@ -447,6 +529,8 @@ export default function LandingAIAgent({ gaEvent }: HyperBotToggleProps) {
447529const MarkdownContent = ( { input } : { input : string } ) => (
448530 < div
449531 className = "prose prose-sm text-sm"
450- dangerouslySetInnerHTML = { { __html : input } }
532+ dangerouslySetInnerHTML = { {
533+ __html : markdownit ( { html : true } ) . render ( input )
534+ } }
451535 />
452536) ;
0 commit comments